Repository: SwanHubX/SwanLab (language: python)

Issue:
[REQUEST] 系统信息和依赖的获取应该由操作员完成，并且接入cloud云端同步接口
## 🤩 Features description [Please make everyone to understand it]

删除watchdoog的一小步
[BUG] 上传媒体文件的409 error
在一些情况下，log媒体文件会出现问题，报409 api错误，前端的显示为持续加载中。

![img_v3_02b3_296b2a7a-6dcb-412f-89d2-dfecd8ddb0bg](https://github.com/SwanHubX/SwanLab/assets/58305964/9a7a4bf4-8df0-4e98-aaee-40ef4ec74814)

[BUG] 上传媒体文件的409 error
在一些情况下，log媒体文件会出现问题，报409 api错误，前端的显示为持续加载中。

![img_v3_02b3_296b2a7a-6dcb-412f-89d2-dfecd8ddb0bg](https://github.com/SwanHubX/SwanLab/assets/58305964/9a7a4bf4-8df0-4e98-aaee-40ef4ec74814)



Discussion / hints:

哪些情况下？
> 哪些情况下？

还没找到能稳定复现的Demo，目前已知主要出现在log `swanlab.Image`的时候。

另一方面，对于已知ERROR的媒体文件，需有占位图。
在使用fastai集成时，在train中途Ctrl+C时，会稳定出现409报错；
判断是多线程问题。
哪些情况下？
> 哪些情况下？

还没找到能稳定复现的Demo，目前已知主要出现在log `swanlab.Image`的时候。

另一方面，对于已知ERROR的媒体文件，需有占位图。
在使用fastai集成时，在train中途Ctrl+C时，会稳定出现409报错；
判断是多线程问题。

Fix patch:
diff --git a/requirements.txt b/requirements.txt
index 63e30dd8..8555d54c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,7 @@
 # dat process
 soundfile
 pillow
+matplotlib
 numpy
 
 # web server
@@ -13,7 +14,7 @@ click
 
 # database
 ujson
-PyYAML
+pyyaml
 peewee
 
 
diff --git a/swanlab/api/cos.py b/swanlab/api/cos.py
index 8439c1b4..9db0b991 100644
--- a/swanlab/api/cos.py
+++ b/swanlab/api/cos.py
@@ -15,6 +15,8 @@
 from qcloud_cos.cos_threadpool import SimpleThreadPool
 from datetime import datetime, timedelta
 from typing import List, Dict, Union
+from swanlab.data.modules import MediaBuffer
+from swanlab.log import swanlog
 
 
 class CosClient:
@@ -37,32 +39,34 @@ def __init__(self, data):
         )
         self.__client = CosS3Client(config)
 
-    def upload(self, key: str, local_path):
+    def upload(self, buffer: MediaBuffer):
         """
         上传文件，需要注意的是file_path应该为unix风格而不是windows风格
         开头不能有/
-        :param key: 上传到cos的文件名称
-        :param local_path: 本地文件路径，一般用绝对路径
+        :param buffer: 本地文件的二进制数据
         """
-        key = self.__prefix + '/' + key
-        self.__client.upload_file(
-            Bucket=self.__bucket,
-            Key=key,
-            LocalFilePath=local_path,
-            EnableMD5=False,
-            progress_callback=None
-        )
+        key = "{}/{}".format(self.__prefix, buffer.file_name)
+        try:
+            swanlog.debug("Uploading file: {}".format(key))
+            self.__client.put_object(
+                Bucket=self.__bucket,
+                Key=key,
+                Body=buffer.getvalue(),
+                EnableMD5=False,
+                # 一年
+                CacheControl="max-age=31536000",
+            )
+        except Exception as e:
+            swanlog.error("Upload error: {}".format(e))
 
-    def upload_files(self, keys: List[str], local_paths: List[str]) -> Dict[str, Union[bool, List]]:
+    def upload_files(self, buffers: List[MediaBuffer]) -> Dict[str, Union[bool, List]]:
         """
         批量上传文件，keys和local_paths的长度应该相等
-        :param keys: 上传到cos的文件名称集合
-        :param local_paths: 本地文件路径，需用绝对路径
+        :param buffers: 本地文件的二进制对象集合
         """
-        assert len(keys) == len(local_paths), "keys and local_paths should have the same length"
         pool = SimpleThreadPool()
-        for key, local_path in zip(keys, local_paths):
-            pool.add_task(self.upload, key, local_path)
+        for buffer in buffers:
+            self.upload(buffer)
         pool.wait_completion()
         result = pool.get_result()
         return result
diff --git a/swanlab/api/http.py b/swanlab/api/http.py
index 8284c5a0..909db30b 100644
--- a/swanlab/api/http.py
+++ b/swanlab/api/http.py
@@ -18,6 +18,8 @@
 from swanlab.log import swanlog
 import requests
 
+from swanlab.data.modules import MediaBuffer
+
 
 def decode_response(resp: requests.Response) -> Union[Dict, AnyStr]:
     """
@@ -158,31 +160,25 @@ def __get_cos(self):
         cos = self.get(f"/project/{self.groupname}/{self.projname}/runs/{self.exp_id}/sts")
         self.__cos = CosClient(cos)
 
-    def upload(self, key: str, local_path):
+    def upload(self, buffer: MediaBuffer):
         """
         上传文件，需要注意的是file_path应该为unix风格而不是windows风格
-        开头不能有/，即使有也会被去掉
-        :param key: 上传到cos的文件名称
-        :param local_path: 本地文件路径，一般用绝对路径
+        :param buffer: 自定义文件内存对象
         """
-        if key.startswith("/"):
-            key = key[1:]
         if self.__cos.should_refresh:
             self.__get_cos()
-        return self.__cos.upload(key, local_path)
+        return self.__cos.upload(buffer)
 
-    def upload_files(self, keys: list, local_paths: list) -> Dict[str, Union[bool, List]]:
+    def upload_files(self, buffers: List[MediaBuffer]) -> Dict[str, Union[bool, List]]:
         """
         批量上传文件，keys和local_paths的长度应该相等
-        :param keys: 上传到cos
-        :param local_paths: 本地文件路径，需用绝对路径
+        :param buffers: 文件内存对象
         :return: 返回上传结果, 包含success_all和detail两个字段，detail为每一个文件的上传结果（通过index索引对应）
         """
         if self.__cos.should_refresh:
             swanlog.debug("Refresh cos...")
             self.__get_cos()
-        keys = [key[1:] if key.startswith("/") else key for key in keys]
-        return self.__cos.upload_files(keys, local_paths)
+        return self.__cos.upload_files(buffers)
 
     def mount_project(self, name: str, username: str = None) -> ProjectInfo:
         self.__username = self.__username if username is None else username
diff --git a/swanlab/api/upload/__init__.py b/swanlab/api/upload/__init__.py
index 976ceb84..ef0e83b8 100644
--- a/swanlab/api/upload/__init__.py
+++ b/swanlab/api/upload/__init__.py
@@ -8,8 +8,8 @@
     上传相关接口
 """
 from ..http import get_http, sync_error_handler
-from .model import ColumnModel
-from typing import List, Tuple, Dict
+from .model import ColumnModel, MediaModel, ScalarModel
+from typing import List
 from swanlab.error import FileError, ApiError
 from swanlab.log import swanlog
 import json
@@ -47,47 +47,27 @@ def upload_logs(logs: List[dict], level: str = "INFO"):
 
 
 @sync_error_handler
-def upload_media_metrics(media_metrics: List[Tuple[dict, str, str, str]]):
+def upload_media_metrics(media_metrics: List[MediaModel]):
     """
     上传指标的媒体数据
-    :param media_metrics: 媒体指标数据，
-        每个元素为元组，第一个元素为指标信息，
-        第二个元素为指标的名称key，经过URI编码
-        第三个元素为指标类型
-        第四个元素为media文件夹路径
+    :param media_metrics: 媒体指标数据集合
     """
     http = get_http()
-    # 需要上传的文件路径[key, local_path]
-    file_paths: Dict[str, str] = {}
-    for metric, key, data_type, media_folder in media_metrics:
-        if data_type == "text":
-            # 字符串类型没有文件路径
-            continue
-        if isinstance(metric["data"], str):
-            local_path = metric["data"]
-            metric["data"] = "{}/{}".format(key, metric["data"])
-            # 将文件路径添加到files_path中
-            file_paths[metric["data"]] = os.path.join(media_folder, key, local_path)
-        else:
-            local_paths = metric['data']
-            metric['data'] = ["{}/{}".format(key, x) for x in local_paths]
-            for i, local_path in enumerate(local_paths):
-                file_paths[metric['data'][i]] = os.path.join(media_folder, key, local_path)
-    # 上传文件，先上传资源文件，再上传指标信息
-    keys = list(file_paths.keys())
-    local_paths = list(file_paths.values())
-    http.upload_files(keys, local_paths)
+    buffers = []
+    for media in media_metrics:
+        media.buffers and buffers.extend(media.buffers)
+    http.upload_files(buffers)
     # 上传指标信息
-    http.post(house_url, create_data([x[0] for x in media_metrics], "media"))
+    http.post(house_url, create_data([x.to_dict() for x in media_metrics], MediaModel.type.value))
 
 
 @sync_error_handler
-def upload_scalar_metrics(scalar_metrics: List[dict]):
+def upload_scalar_metrics(scalar_metrics: List[ScalarModel]):
     """
     上传指标的标量数据
     """
     http = get_http()
-    data = create_data(scalar_metrics, "scalar")
+    data = create_data([x.to_dict() for x in scalar_metrics], ScalarModel.type.value)
     http.post(house_url, data)
 
 
@@ -153,5 +133,8 @@ def upload_column(columns: List[ColumnModel]):
     "upload_media_metrics",
     "upload_scalar_metrics",
     "upload_files",
-    "upload_column"
+    "upload_column",
+    "ScalarModel",
+    "MediaModel",
+    "ColumnModel"
 ]
diff --git a/swanlab/api/upload/model.py b/swanlab/api/upload/model.py
index 9b822059..9c0989b1 100644
--- a/swanlab/api/upload/model.py
+++ b/swanlab/api/upload/model.py
@@ -7,6 +7,10 @@
 @Description:
     上传请求模型
 """
+from enum import Enum
+from typing import List
+
+from swanlab.data.modules import MediaBuffer
 
 
 class ColumnModel:
@@ -21,12 +25,13 @@ def __init__(self, key, column_type: str, error: dict = None):
         :param error: 错误信息，如果错误信息不为None
         """
         self.key = key
-        if column_type == "DEFAULT":
-            column_type = "FLOAT"
         self.column_type = column_type
         self.error = error
 
     def to_dict(self):
+        """
+        序列化为Dict
+        """
         return {
             "key": self.key,
             "type": self.column_type,
@@ -35,3 +40,88 @@ def to_dict(self):
             "type": self.column_type,
             "error": self.error
         }
+
+
+class MetricType(Enum):
+    """
+    指标类型枚举
+    """
+    SCALAR = "scalar"
+    """
+    标量指标
+    """
+    MEDIA = "media"
+    """
+    媒体指标
+    """
+    LOG = "log"
+    """
+    日志数据
+    """
+
+
+class MediaModel:
+    """
+    媒体指标信息上传模型
+    """
+    type = MetricType.MEDIA
+
+    def __init__(
+        self,
+        metric: dict,
+        key: str,
+        key_encoded: str,
+        step: int,
+        epoch: int,
+        buffers: List[MediaBuffer] = None
+    ):
+        self.metric = metric
+        self.step = step
+        self.epoch = epoch
+        self.key = key
+        """
+        真实的指标名称
+        """
+        self.key_encoded = key_encoded
+        """
+        编码后路径安全的指标名称
+        """
+        self.buffers = buffers
+        """
+        原始数据，可能为None
+        """
+
+    def to_dict(self):
+        """
+        序列化
+        """
+        return {
+            **self.metric,
+            "key": self.key,
+            "index": self.step,
+            "epoch": self.epoch
+        }
+
+
+class ScalarModel:
+    """
+    标量指标信息上传模型
+    """
+    type = MetricType.SCALAR
+
+    def __init__(self, metric: dict, key: str, step: int, epoch: int):
+        self.metric = metric
+        self.key = key
+        self.step = step
+        self.epoch = epoch
+
+    def to_dict(self):
+        """
+        序列化
+        """
+        return {
+            **self.metric,
+            "key": self.key,
+            "index": self.step,
+            "epoch": self.epoch
+        }
diff --git a/swanlab/cloud/_log_collector.py b/swanlab/cloud/log_collector.py
similarity index 80%
rename from swanlab/cloud/_log_collector.py
rename to swanlab/cloud/log_collector.py
index 1fc50c83..50d25959 100644
--- a/swanlab/cloud/_log_collector.py
+++ b/swanlab/cloud/log_collector.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 r"""
 @DATE: 2024/4/1 19:40
-@File: _log_collector.py
+@File: log_collector.py
 @IDE: pycharm
 @Description:
     日志集合和上传记录器
@@ -30,12 +30,13 @@ class LogCollectorTask(ThreadTaskABC):
     日志上传的地址
     """
 
-    def __init__(self):
+    def __init__(self, upload_type=UploadType):
         self.container: List[LogQueue.MsgType] = []
         """
         日志容器，存储从管道中获取的日志信息
         """
-        self.__now_task = None
+        self.lock = False
+        self.upload_type = upload_type
 
     @staticmethod
     def report_known_error(errors: List[SyncError]):
@@ -52,23 +53,27 @@ def report_known_error(errors: List[SyncError]):
 
     def upload(self):
         """
+        NOTE 此函数运行在其他线程
         上传事件处理
         将收集到的所有上传事件统一触发，上传日志信息
         所有的请求都是网络请求，因此需要异步处理
+        对于执行的任务，除了column_type为同步执行，其他为并发执行
         """
         # 根据日志类型进行降重
-        upload_tasks_dict = {x: [] for x in UploadType}
+        upload_tasks_dict = {x: [] for x in self.upload_type}
         # 检查每一个上传结果
         success_tasks_type = []
         # 已知错误列表
         known_errors = []
         # ---------------------------------- 处理upload任务 ----------------------------------
 
+        # 聚合所有的上传任务
         for msg in self.container:
-            if msg[0] in UploadType:
+            if msg[0] in self.upload_type:
                 upload_tasks_dict[msg[0]].extend(msg[1])
+
         tasks_key_list = [key for key in upload_tasks_dict if len(upload_tasks_dict[key]) > 0]
-        
+
         # 同步执行所有的上传任务
         results = [x.value['upload'](upload_tasks_dict[x]) for x in tasks_key_list]
         for index, result in enumerate(results):
@@ -79,10 +84,10 @@ def upload(self):
                 continue
             # 如果出现其他问题，没有办法处理，就直接跳过，但是会有警告
             elif e is not None:
-                # error = f"{tasks_key_list[index].name} error: {e}, it might be a swanlab bug, data will be lost!"
-                # swanlog.error(error)
+                error = f"{tasks_key_list[index].name} error: {e}, it might be a swanlab bug, data will be lost!"
+                swanlog.error(error)
                 # continue
-                raise e
+                # raise e
             # 标记所有已经成功的任务
             success_tasks_type.append(tasks_key_list[index])
 
@@ -102,15 +107,17 @@ def task(self, u: ThreadUtil, *args):
         self.container.extend(u.queue.get_all())
         # print("线程" + u.name + "获取到的日志信息: ", self.container)
         if u.timer.can_run(self.UPLOAD_TIME, len(self.container) == 0):
-            self.upload()
-
-    @property
-    def lock(self):
-        return self.__now_task is not None
+            self.lock = True
+            try:
+                self.upload()
+            except Exception as e:
+                swanlog.error(f"upload error: {e}")
+            self.lock = False
 
     def callback(self, u: ThreadUtil, *args):
         """
-        回调函数，用于线程结束时的回调
+        回调函数，用于结束时的回调
+        NOTE 此函数运行在主线程
         :param u: 线程工具类
         """
         # 如果当前上传任务正在进行，等待上传任务结束
diff --git a/swanlab/cloud/start_thread.py b/swanlab/cloud/start_thread.py
index 68fd1cef..909d9ce3 100644
--- a/swanlab/cloud/start_thread.py
+++ b/swanlab/cloud/start_thread.py
@@ -12,7 +12,7 @@
 from typing import List, Tuple, Callable, Dict
 from .utils import ThreadUtil
 from .utils import LogQueue, TimerFlag
-from ._log_collector import LogCollectorTask
+from .log_collector import LogCollectorTask
 from queue import Queue
 from swanlab.log import swanlog
 
diff --git a/swanlab/data/__init__.py b/swanlab/data/__init__.py
index 45e544e9..5ee93da0 100644
--- a/swanlab/data/__init__.py
+++ b/swanlab/data/__init__.py
@@ -12,8 +12,6 @@
     Audio,
     Image,
     Text,
-    Video,
-    Object3D,
 )
 
 from .sdk import (
diff --git a/swanlab/data/callback_cloud.py b/swanlab/data/callback_cloud.py
index fbbfb252..035c7877 100644
--- a/swanlab/data/callback_cloud.py
+++ b/swanlab/data/callback_cloud.py
@@ -10,8 +10,7 @@
 from .run.callback import MetricInfo, ColumnInfo
 from swanlab.cloud import UploadType
 from swanlab.error import ApiError
-from swanlab.api.upload.model import ColumnModel
-from urllib.parse import quote
+from swanlab.api.upload.model import ColumnModel, ScalarModel, MediaModel
 from swanlab.api import LoginInfo, create_http, terminal_login
 from swanlab.api.upload import upload_logs
 from swanlab.log import swanlog
@@ -27,6 +26,7 @@
 from swanlab.db import Experiment
 from swanlab.utils import create_time
 from swanlab.package import get_package_version, get_package_latest_version
+import json
 import sys
 import os
 import io
@@ -160,23 +160,42 @@ def _write_call_call(message):
             show_button_html(experiment_url)
 
     def on_column_create(self, column_info: ColumnInfo):
-        self.pool.queue.put(
-            (UploadType.COLUMN, [ColumnModel(column_info.key, column_info.data_type.upper(), column_info.error)])
+        error = None
+        if column_info.error is not None:
+            error = {"data_class": column_info.error.got, "excepted": column_info.error.expected}
+        column = ColumnModel(
+            key=column_info.key,
+            column_type=column_info.chart.value.column_type,
+            error=error
         )
+        self.pool.queue.put((UploadType.COLUMN, [column]))
 
     def on_metric_create(self, metric_info: MetricInfo):
         super(CloudRunCallback, self).on_metric_create(metric_info)
+        # 有错误就不上传
         if metric_info.error:
             return
-        new_data = metric_info.metric
-        new_data["key"] = metric_info.key
-        new_data["index"] = metric_info.step
-        new_data["epoch"] = metric_info.epoch
-        if metric_info.data_type == "default":
-            return self.pool.queue.put((UploadType.SCALAR_METRIC, [new_data]))
-        key = quote(metric_info.key, safe="")
-        data = (new_data, key, metric_info.data_type, metric_info.static_dir)
-        self.pool.queue.put((UploadType.MEDIA_METRIC, [data]))
+        metric = metric_info.metric
+        key = metric_info.column_info.key
+        key_encoded = metric_info.key
+        step = metric_info.step
+        epoch = metric_info.epoch
+        # 标量折线图
+        if metric_info.column_info.chart == metric_info.column_info.chart.LINE:
+            scalar = ScalarModel(metric, key, step, epoch)
+            return self.pool.queue.put((UploadType.SCALAR_METRIC, [scalar]))
+        # 媒体指标数据
+
+        # -------------------------- 🤡这里是一点小小的💩 --------------------------
+        # 要求上传时的文件路径必须带key_encoded前缀
+        if metric_info.buffers is not None:
+            metric = json.loads(json.dumps(metric))
+            for i, d in enumerate(metric["data"]):
+                metric["data"][i] = "{}/{}".format(key_encoded, d)
+        # ------------------------------------------------------------------------
+
+        media = MediaModel(metric, key, key_encoded, step, epoch, metric_info.buffers)
+        self.pool.queue.put((UploadType.MEDIA_METRIC, [media]))
 
     def on_stop(self, error: str = None):
         # 打印信息
diff --git a/swanlab/data/callback_local.py b/swanlab/data/callback_local.py
index 57aacd67..3a309fd7 100644
--- a/swanlab/data/callback_local.py
+++ b/swanlab/data/callback_local.py
@@ -43,9 +43,9 @@ def _error_print(tp):
         """
         # 如果是KeyboardInterrupt异常
         if tp == KeyboardInterrupt:
-            swanlog.error("KeyboardInterrupt by user")
+            swanlog.info("KeyboardInterrupt by user")
         else:
-            swanlog.error("Error happened while training")
+            swanlog.info("Error happened while training")
 
     @staticmethod
     def _init_logdir(logdir: str = None) -> str:
@@ -119,7 +119,7 @@ def before_init_experiment(
         description: str,
         num: int,
         suffix: str,
-        setter: Callable[[str, str, str, str], None]
+        setter: Callable[[str, str, str, str], None],
     ):
         requirements_path = self.settings.requirements_path
         metadata_path = self.settings.metadata_path
@@ -142,8 +142,11 @@ def on_run(self):
             os.mkdir(self.settings.log_dir)
 
     def on_metric_create(self, metric_info: MetricInfo):
+        # 出现任何错误直接返回
         if metric_info.error:
             return
+        # ---------------------------------- 保存指标数据 ----------------------------------
+
         self.settings.mkdir(os.path.dirname(metric_info.metric_path))
         self.settings.mkdir(os.path.dirname(metric_info.summary_path))
         with open(metric_info.summary_path, "w+") as f:
@@ -151,6 +154,19 @@ def on_metric_create(self, metric_info: MetricInfo):
         with open(metric_info.metric_path, "a") as f:
             f.write(json.dumps(metric_info.metric, ensure_ascii=False) + "\n")
 
+        # ---------------------------------- 保存媒体字节流数据 ----------------------------------
+        if metric_info.buffers is None:
+            return
+        for i, r in enumerate(metric_info.buffers):
+            if r is None:
+                continue
+            # 组合路径
+            path = os.path.join(self.settings.media_dir, metric_info.key)
+            os.makedirs(path, exist_ok=True)
+            # 写入数据
+            with open(os.path.join(path, metric_info.metric["data"][i]), "wb") as f:
+                f.write(r.getvalue())
+
     def on_stop(self, error: str = None):
         """
         训练结束，取消系统回调
diff --git a/swanlab/data/modules/__init__.py b/swanlab/data/modules/__init__.py
index d74852e4..6fbc91b3 100644
--- a/swanlab/data/modules/__init__.py
+++ b/swanlab/data/modules/__init__.py
@@ -1,15 +1,24 @@
-from .base import BaseType
+from .base import BaseType, MediaType, ParseResult, MediaBuffer
 from .audio import Audio
 from .image import Image
 from .text import Text
-from .video import Video
-from .object_3d import Object3D
-# from .video import Video
-from typing import Protocol, Union, List
+from .line import Line, FloatConvertible
+from typing import Union, List
+from .wrapper import DataWrapper, ErrorInfo
 
+DataType = Union[int, float, FloatConvertible, BaseType, List[BaseType]]
+ChartType = BaseType.Chart
 
-class FloatConvertible(Protocol):
-    def __float__(self) -> float: ...
-
-
-DataType = Union[float, FloatConvertible, int, BaseType, List[BaseType]]
+__all__ = [
+    "FloatConvertible",
+    "DataWrapper",
+    "MediaType",
+    "Image",
+    "Audio",
+    "Text",
+    "Line",
+    "DataType",
+    "ChartType",
+    "ErrorInfo",
+    "MediaBuffer"
+]
diff --git a/swanlab/data/modules/_utils.py b/swanlab/data/modules/_utils.py
deleted file mode 100644
index 0653db0e..00000000
--- a/swanlab/data/modules/_utils.py
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024/5/5 22:47
-@File: _utils.py
-@IDE: pycharm
-@Description:
-    工具函数
-"""
-import io
-import hashlib
-
-
-def get_file_hash_path(file_path: str) -> str:
-    """计算并返回给定文件的SHA-256哈希值。"""
-
-    hash_sha256 = hashlib.sha256()
-    with open(file_path, "rb") as f:  # 以二进制读取模式打开文件
-        while chunk := f.read(8192):  # 读取文件的小块进行处理
-            hash_sha256.update(chunk)
-    return hash_sha256.hexdigest()
-
-
-def get_file_hash_numpy_array(array) -> str:
-    """计算并返回给定NumPy数组的SHA-256哈希值。"""
-
-    hash_sha256 = hashlib.sha256()
-    # 将NumPy数组转换为字节串，然后更新哈希值
-    hash_sha256.update(array.tobytes())
-    return hash_sha256.hexdigest()
-
-
-def get_file_hash_pil(image) -> str:
-    """计算并返回给定PIL.Image对象的SHA-256哈希值。"""
-
-    hash_sha256 = hashlib.sha256()
-    # 将图像转换为字节数据
-    with io.BytesIO() as buffer:
-        image.save(buffer, format="PNG")  # 可以选择其他格式，如'JPEG'
-        hash_sha256.update(buffer.getvalue())
-    return hash_sha256.hexdigest()
diff --git a/swanlab/data/modules/audio.py b/swanlab/data/modules/audio.py
deleted file mode 100644
index 5f011299..00000000
--- a/swanlab/data/modules/audio.py
+++ /dev/null
@@ -1,239 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Date: 2024-01-22 15:01:36
-IDE: VSCode
-FilePath: /SwanLab/swanlab/data/modules/audio.py
-Description:
-    音频数据解析
-"""
-from .base import BaseType
-from ._utils import get_file_hash_numpy_array, get_file_hash_path
-import os
-from typing import Union, List
-
-### 以下为音频数据解析的依赖库
-import soundfile as sf
-import numpy as np
-import json
-import os
-
-
-class Audio(BaseType):
-    """Audio class constructor
-
-    Parameters
-    ----------
-    data_or_path: str or numpy.array
-        Path to an audio file or numpy array of audio data.
-    sample_rate: int
-            Sample rate of the audio data. Required when input is numpy array.
-    caption: str
-        Caption for the audio.
-    """
-
-    def __init__(
-        self,
-        data_or_path: Union[str, np.ndarray, List["Audio"]],
-        sample_rate: int = 44100,
-        caption: str = None,
-    ):
-        """Accept a path to an audio file on a numpу array of audio data."""
-        super().__init__(data_or_path)
-        self.audio_data = None
-        self.sample_rate = sample_rate
-        self.caption = self.__convert_caption(caption)
-
-    def get_data(self):
-        # 如果传入的是Audio类列表
-        if isinstance(self.value, list):
-            return self.get_data_list()
-        self.__preprocess(self.value)
-        # 判断是否要保存(mode='disabled'时不保存)
-        if not self.settings.should_save:
-            return
-        hash_name = (
-            get_file_hash_numpy_array(self.audio_data)[:16]
-            if isinstance(self.audio_data, np.ndarray)
-            else get_file_hash_path(self.audio_data)[:16]
-        )
-        save_dir = os.path.join(self.settings.static_dir, self.tag)
-        save_name = f"audio-step{self.step}-{hash_name}.wav"
-        if not os.path.exists(save_dir):
-            os.mkdir(save_dir)
-        save_path = os.path.join(save_dir, save_name)
-
-        # 保存音频数据到指定目录
-        self.__save(save_path)
-        return save_name
-
-    def expect_types(self, *args, **kwargs) -> list:
-        return ["str", "numpy.ndarray"]
-
-    def __convert_caption(self, caption):
-        """将caption转换为字符串"""
-        # 如果类型是字符串，则不做转换
-        if isinstance(caption, str):
-            caption = caption
-        # 如果类型是数字，则转换为字符串
-        elif isinstance(caption, (int, float)):
-            caption = str(caption)
-        # 如果类型是None，则转换为默认字符串
-        elif caption is None:
-            caption = None
-        else:
-            raise TypeError("caption must be a string, int or float.")
-        return caption.strip() if caption else None
-
-    def __preprocess(self, data_or_path):
-        """
-        根据输入不同的输入类型进行不同处理
-        """
-        if isinstance(data_or_path, str):
-            # 如果输入为路径字符串
-            # 根据输入是否为 json , 选择不同的加载方式
-            if data_or_path.endswith(".json"):
-                self.__load_from_json(data_or_path)
-            else:
-                self.audio_data, self.sample_rate = self.__load_audio_from_path(data_or_path)
-
-        elif isinstance(data_or_path, np.ndarray):
-            # 如果输入为numpy array ，要求输入为 (num_channels, num_frames) 的形式
-            # 支持单声道 或 双声道 两种形式
-
-            SF_SUPPORT_DTYPE = [np.dtype(d) for d in ["float32", "float64", "int16", "int32"]]
-
-            if data_or_path.dtype not in SF_SUPPORT_DTYPE:
-                raise TypeError(
-                    f"Invalid numpy array for the audio data, support dtype is {SF_SUPPORT_DTYPE}, but got {data_or_path.dtype}"
-                )
-
-            # 如果data_or_path是一维, 则reshape为2维
-            if len(data_or_path.shape) == 1:
-                data_or_path = data_or_path.reshape(1, -1)
-
-            # 获取通道数
-            num_channels = data_or_path.shape[0]
-
-            if num_channels != 2 and num_channels != 1:
-                raise TypeError("Invalid numpy array for the audio data, support shape is (num_channels, num_frames)")
-            if self.sample_rate is None:
-                raise TypeError("sample_rate must be provided when input is numpy array while constructing Audio()")
-            self.audio_data = data_or_path
-        else:
-            # 以上都不是，则报错
-            raise TypeError("Unsupported audio type. Please provide a valid path or numpy array.")
-
-    def __load_audio_from_path(self, path):
-        """判断字符串是否为正确的音频文件路径，如果是则转换为numpy array，如果不是则报错"""
-        try:
-            audio_data, sample_rate = sf.read(path)
-            # 转置为 (num_channels, num_frames) 的形式
-            audio_data = audio_data.T
-            return audio_data, sample_rate
-
-        except Exception as e:
-            raise ValueError(f"Invalid audio path: {path}") from e
-
-    def __to_json(self, audio_path, json_path):
-        """将音频元数据转换为json文件"""
-        audio_json = {}
-        audio_json["type"] = "audio"
-        audio_json["file_path"] = audio_path
-        audio_json["sample_rate"] = self.sample_rate
-        audio_json["duration"] = self.audio_data.shape[1]
-
-        with open(json_path, "w") as f:
-            json.dump(audio_json, f)
-
-    def __load_from_json(self, json_path):
-        """从json文件中加载音频元数据"""
-        with open(json_path, "r") as f:
-            audio_json = json.load(f)
-        json_sample_rate = audio_json["sample_rate"]
-        file_path = audio_json["file_path"]
-        audio_data, data_sample_rate = self.__load_audio_from_path(file_path)
-        if json_sample_rate != data_sample_rate:
-            raise TypeError("sample_rate in json file is not equal to the sample_rate of the audio file")
-        self.audio_data = audio_data
-        self.sample_rate = data_sample_rate
-
-    def __save(self, save_path):
-        """
-        保存媒体资源文件 .wav 到指定路径
-        audio-step{}.wav
-        """
-
-        try:
-            write_audio_data = self.audio_data.T
-            sf.write(save_path, write_audio_data, self.sample_rate)
-        except Exception as e:
-            raise ValueError(f"Could not save the audio file to the path: {save_path}") from e
-
-    def get_more(self, *args, **kwargs) -> dict:
-        """返回config数据"""
-        # 如果传入的是Audio类列表
-        if isinstance(self.value, list):
-            return self.get_more_list()
-        else:
-            return (
-                {
-                    "caption": self.caption,
-                }
-                if self.caption is not None
-                else None
-            )
-
-    def get_namespace(self, *args, **kwargs) -> str:
-        """设定分组名"""
-        return "Audio"
-
-    def get_chart_type(self) -> str:
-        """设定图表类型"""
-        return self.chart.audio
-
-    # The following is a temporary code that may be used in the future.
-
-    # def plot_spectrogram(self, plot_save_path):
-    #     """获取音频的频谱"""
-    #     num_channels = self.audio_data.shape[0]
-    #     sample_rate = self.sample_rate
-    #     figure, axes = plt.subplots(num_channels, 1)
-    #     if num_channels == 1:
-    #         axes = [axes]
-    #     for c in range(num_channels):
-    #         axes[c].specgram(self.audio_data[c], Fs=sample_rate)
-    #         if num_channels > 1:
-    #             axes[c].set_ylabel(f"Channel {c+1}")
-    #     figure.savefig(plot_save_path)
-    #     plt.close()
-
-    # def get_play_audio(self):
-    #     """获取可播放音频类型"""
-    #     from IPython.display import Audio as AudioDisplay
-
-    #     try:
-    #         return AudioDisplay(self.audio_data, rate=self.sample_rate)
-    #     except Exception as e:
-    #         raise ValueError(f"Could not play the audio file") from e
-
-    # def save_all(self, save_path):
-
-    #     """
-    #     保存静态资源文件 .wav 到指定路径
-    #     audio-{tag}-{step}.wav
-    #     audio-{tag}-{step}.json
-    #     """
-    #     try:
-    #         write_audio_data = self.audio_data.T
-    #         sf.write(save_path, write_audio_data, self.sample_rate, subtype="PCM_16")
-
-    #         # 这边存 json 的时候放的是相对路径
-    #         json_path = save_path.replace(".wav", ".json")
-    #         save_relative_path = os.path.relpath(save_path, self.settings.root_dir)
-    #         self.to_json(save_relative_path, json_path)
-
-    #         # 保存频谱图
-    #         spectrogram_save_path = save_path.replace(".wav", "-spectrogram.png")
-    #         self.plot_spectrogram(spectrogram_save_path)
-    #     except Exception as e:
-    #         raise ValueError(f"Could not save the audio file to the path: {save_path}") from e
diff --git a/swanlab/data/modules/audio/__init__.py b/swanlab/data/modules/audio/__init__.py
new file mode 100644
index 00000000..c295d7ed
--- /dev/null
+++ b/swanlab/data/modules/audio/__init__.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 02:34
+@File: __init__.py
+@IDE: pycharm
+@Description:
+    音频模块
+"""
+from ..base import MediaType, MediaBuffer, DataSuite as D
+from typing import Union
+import soundfile as sf
+import numpy as np
+
+
+class Audio(MediaType):
+    SF_SUPPORT_DTYPE = [np.dtype(d) for d in ["float32", "float64", "int16", "int32"]]
+
+    def __init__(
+        self,
+        data_or_path: Union[str, np.ndarray],
+        sample_rate: int = 44100,
+        caption: str = None,
+    ):
+        """Audio class constructor
+
+        Parameters
+        ----------
+        data_or_path: str or numpy.ndarray
+            Path to an audio file or numpy array of audio data.
+        sample_rate: int
+                Sample rate of the audio data. Required when input is numpy array.
+        caption: str
+            Caption for the audio.
+        """
+        super().__init__()
+        if isinstance(data_or_path, str):
+            # 如果输入为路径字符串
+            try:
+                audio_data, sample_rate = sf.read(data_or_path)
+                # 转置为 (num_channels, num_frames) 的形式
+                audio_data = audio_data.T
+            except Exception as e:
+                raise ValueError(f"Invalid audio path: {data_or_path}") from e
+        elif isinstance(data_or_path, np.ndarray):
+            # 如果输入为numpy array ，要求输入为 (num_channels, num_frames) 的形式
+            # 支持单声道 或 双声道 两种形式
+
+            if data_or_path.dtype not in self.SF_SUPPORT_DTYPE:
+                e = (
+                    f"Invalid numpy array for the audio data, support dtype is {self.SF_SUPPORT_DTYPE}, "
+                    f"but got {data_or_path.dtype}"
+                )
+                raise TypeError(e)
+
+            # 如果data_or_path是一维, 则reshape为2维
+            if len(data_or_path.shape) == 1:
+                data_or_path = data_or_path.reshape(1, -1)
+
+            # 获取通道数
+            num_channels = data_or_path.shape[0]
+
+            if num_channels != 2 and num_channels != 1:
+                raise TypeError("Invalid numpy array for the audio data, support shape is (num_channels, num_frames)")
+            if sample_rate is None:
+                raise TypeError("sample_rate must be provided when input is numpy array while constructing Audio()")
+            audio_data = data_or_path
+        else:
+            # 以上都不是，则报错
+            raise TypeError("Unsupported audio type. Please provide a valid path or numpy array.")
+        self.audio_data = audio_data
+        """
+        转换为矩阵后的数据
+        """
+        self.buffer = MediaBuffer()
+        sf.write(self.buffer, audio_data.T, sample_rate, format="wav")
+        self.sample_rate = sample_rate
+        self.caption = D.check_caption(caption)
+
+    # ---------------------------------- 覆写方法 ----------------------------------
+    def parse(self):
+        # 文件名称
+        hash_name = D.get_hash_by_ndarray(self.audio_data)[:16]
+        save_name = f"audio-step{self.step}-{hash_name}.wav"
+        return save_name, self.buffer
+
+    def get_more(self):
+        """返回more数据"""
+        # 如果传入的是Audio类列表
+        return {"caption": self.caption} if self.caption is not None else None
+
+    def get_section(self):
+        """设定分组名"""
+        return "Audio"
+
+    def get_chart(self):
+        """设定图表类型"""
+        return self.Chart.AUDIO
diff --git a/swanlab/data/modules/base.py b/swanlab/data/modules/base.py
index 9994b5b6..b26f25dd 100644
--- a/swanlab/data/modules/base.py
+++ b/swanlab/data/modules/base.py
@@ -6,136 +6,295 @@
 @IDE: vscode
 @Description:
     SwanLab数据基类
+    数据的转换流程是：
+    1. 如果传入的数据在这之前已经有key，跳转步骤3
+    2. 没有key的数据，需要判断当前数据是否满足要求，因此需要转换一次，跳转步骤3
+    3. 转换一次，判断与之前的数据是否一致，因此还需要转换一次
+    可以看到有两次转换，如果每次都是耗时操作，那么会导致性能问题，所以对于单个类而言，上次转换的结果会被保存
 """
 from abc import ABC, abstractmethod
 from ..settings import SwanDataSettings
-from .chart import Chart
-from urllib.parse import quote
+from typing import List, Dict, Optional, ByteString, Union, Tuple
+from swanlab.log import swanlog
+from enum import Enum
+from io import BytesIO
+import hashlib
+import math
+import io
 
 
-class BaseType(ABC):
-    """SwanLab数据基类，完成数据解析，拿到图表类型、namespace和解析后的数据"""
+class DataSuite:
+    """
+    BaseType在处理时的通用工具套件
+    """
 
-    def __init__(self, value, *args, **kwargs):
+    @staticmethod
+    def get_hash_by_bytes(file_byte: ByteString, chunk: int = 4096) -> str:
+        """
+        计算并返回给定字节流的SHA-256哈希值
+        :param file_byte: 字节流
+        :param chunk: 一次读取的字节数，用于加快处理速度
+        """
+
+        hash_sha256 = hashlib.sha256()
+        # 更新哈希值，为了处理速度，只读取一小块
+        hash_sha256.update(file_byte[:chunk])
+        return hash_sha256.hexdigest()
+
+    @staticmethod
+    def get_hash_by_ndarray(array) -> str:
+        """
+        计算并返回给定NumPy数组的SHA-256哈希值
+        :param array: NumPy数组
+        """
+
+        hash_sha256 = hashlib.sha256()
+        # 将NumPy数组转换为字节串，然后更新哈希值
+        hash_sha256.update(array.tobytes())
+        return hash_sha256.hexdigest()
+
+    @staticmethod
+    def get_hash_by_pil(image, f="PNG") -> str:
+        """计算并返回给定PIL.Image对象的SHA-256哈希值。
+        :param image: PIL.Image对象
+        :param f: 图像格式，默认为PNG，可选为JPEG
+        """
+
+        hash_sha256 = hashlib.sha256()
+        # 将图像转换为字节数据
+        with io.BytesIO() as buffer:
+            image.save(buffer, format=f)
+            hash_sha256.update(buffer.getvalue())
+        return hash_sha256.hexdigest()
+
+    @staticmethod
+    def is_nan(data):
+        """判断data是否为nan"""
+        return isinstance(data, (int, float)) and math.isnan(data)
+
+    @staticmethod
+    def is_inf(data):
+        """判断data是否为inf"""
+        return isinstance(data, (int, float)) and math.isinf(data)
+
+    @staticmethod
+    def check_caption(caption) -> Optional[str]:
+        """
+        检查caption是否符合要求
+        :param caption: 标题
+        :return: 返回处理后的标题
+        :raises TypeError: caption不符合要求
+        """
+        if isinstance(caption, str):
+            caption = caption
+        # 如果类型是数字，则转换为字符串
+        elif isinstance(caption, (int, float)):
+            caption = str(caption)
+        # 如果类型是None，则转换为默认字符串
+        elif caption is None:
+            caption = None
+        else:
+            raise TypeError("caption must be a string, int or float.")
+        return caption.strip() if caption else None
+
+
+class DynamicProperty:
+    """
+    动态属性，一开始为None，动态设置
+    但是可以保证在转换时都已经被设置
+    """
+
+    def __init__(self):
         # 保存的key
-        self.__tag: str = None
+        self.key: Optional[str] = None
         # 保存的step
-        self.__step: int = None
-        # 受支持的图表类型，不可修改
-        self.__chart: Chart = Chart()
+        self.step: Optional[int] = None
         # 当前运行时配置
-        self.__settings: SwanDataSettings = None
-        # 保存的原始值
-        self.value = value
-        # 标志是否已经被解构
-        self.__extracted: bool = False
-        # get_data方法是否被执行
-        self.__is_get_data: bool = False
-        # get_data的返回值
-        self.__data = None
-
-    def __iter__(self):
-        # 返回一个迭代器，通常是自身
-        if self.__extracted:
-            raise StopIteration("This object has been extracted")
-        return self
-
-    def __next__(self):
-        """解构对象，用法是 namespace, chart_type, config = object"""
-        return self.get_namespace(), self.get_chart_type(), "step", self.get_config()
+        self.settings: Optional[SwanDataSettings] = None
 
-    @property
-    def chart(self) -> Chart:
-        return self.__chart
+    def inject(self, key: str, step: int, settings: SwanDataSettings):
+        """
+        注入属性
+        :param key: key名称
+        :param step: 当前步骤
+        :param settings: 当前运行时配置
+        """
+        self.key = key
+        self.step = step
+        self.settings = settings
 
-    @property
-    def convert(self):
-        if self.__is_get_data is False:
-            self.__data = self.get_data()
-            self.__is_get_data = True
-        return self.__data
+
+class BaseType(ABC, DynamicProperty):
+    """
+    SwanLab数据基类，完成数据解析，拿到图表类型、namespace和解析后的数据
+    所有的数据类都只负责解析，并返回相应的内容
+    """
+
+    class Chart(Enum):
+        """
+        图表选择器
+        """
+
+        class ChartItem:
+            """
+            图表项
+            """
+
+            def __init__(self, chart_type: str, column_type: str):
+                self.chart_type = chart_type
+                """
+                本地标注的图表类型
+                """
+                self.column_type = column_type
+                """
+                上传列信息时，标注的图表类型
+                """
+
+        LINE = ChartItem("line", "FLOAT")
+
+        IMAGE = ChartItem("image", "IMAGE")
+
+        AUDIO = ChartItem("audio", "AUDIO")
+
+        TEXT = ChartItem("text", "TEXT")
+
+    # ---------------------------------- 需要子类实现的方法 ----------------------------------
 
     @abstractmethod
-    def get_data(self, *args, **kwargs):
-        """获取data数据，将返回一个可被JSON序列化的数据"""
+    def parse(self) -> Tuple[Union[str, float], Optional[BytesIO]]:
+        """
+        解析数据
+        :raises DataTypeError: 传入的数据类型不符合要求
+        """
         pass
 
-    def get_namespace(self, *args, **kwargs) -> str:
-        """获取namespace数据，将返回一个可被JSON序列化的数据"""
-        return "default"
-
     @abstractmethod
-    def get_chart_type(self, *args, **kwargs) -> str:
-        """获取chart_type数据，应该返回一个字符串"""
+    def get_chart(self) -> "Chart":
+        """
+        获取图表类型
+        """
         pass
 
-    def get_config(self, *args, **kwargs) -> dict:
-        """获取图表的config数据，应该返回一个字典"""
-        return {}
+    # ---------------------------------- 可选覆写方法 ----------------------------------
+    # noinspection PyMethodMayBeStatic
+    def get_section(self) -> str:
+        """
+        获取默认的section名称，可能在后续处理时，与实际的section名称不同
+        """
+        return "default"
 
-    def get_more(self, *args, **kwargs):
-        """代表当前步骤的此数据支持标注的更多内容，应该返回一个字典，或者为None"""
+    # noinspection PyMethodMayBeStatic
+    def get_config(self) -> Optional[Dict]:
+        """
+        获取图表的config配置信息，应该返回一个字典，或者为None
+        """
         return None
 
-    @abstractmethod
-    def expect_types(self, *args, **kwargs) -> list:
-        """输入期望的数据类型，list类型，例如["int", "float"]"""
-        pass
+    def get_more(self) -> Optional[Dict]:
+        """
+        代表当前步骤的此数据支持标注的更多内容，应该返回一个字典，或者为None
+        """
+        return None
+
+
+class MediaType(BaseType):  # noqa
+    """
+    媒体类型，用于区分标量和媒体，不用做实例化，应该由子类继承
+    """
+    pass
+
+
+class MediaBuffer(BytesIO):
+    def __init__(self):
+        super().__init__()
+        self.__file_name = None
 
-    def get_data_list(self):
-        """单step多数据时，返回多个数据的列表"""
-        datas = []
-        for i in self.value:
-            try:
-                i.settings = self.settings
-            except AttributeError:
-                pass
-            try:
-                i.tag = self.tag
-            except AttributeError:
-                pass
-            try:
-                i.step = self.step
-            except AttributeError:
-                pass
-            datas.append(i.get_data())
-        return [i.get_data() for i in self.value]
-
-    def get_more_list(self):
-        """单step多数据时，返回多个数据更多信息"""
-        return [i.get_more() for i in self.value]
-
-    # ---------------------------------- 向子类暴露tag、step、create_time,他们的值只能修改一次 ----------------------------------
     @property
-    def tag(self):
-        return self.__tag
+    def file_name(self):
+        if self.__file_name is None:
+            raise ValueError("file_name is not set")
+        return self.__file_name
+
+    @file_name.setter
+    def file_name(self, value):
+        if not isinstance(value, str) or not value:
+            raise TypeError(f"Expected str, but got {type(value)}")
+        if self.__file_name is not None:
+            # 此时意味着使用类似 [] * 2 的操作复制了多个相同的实例，这允许，但不推荐
+            swanlog.warning("You are log duplicate and same instance, this is not recommended")
+        self.__file_name = value
+
+
+class ParseResult:
+    """
+    数据解析结果
+    """
+
+    def __init__(
+        self,
+        section: str = None,
+        chart: BaseType.Chart = None,
+        data: Union[List[str], float] = None,
+        config: Optional[List[Dict]] = None,
+        more: Optional[List[Dict]] = None,
+        buffers: Optional[List[MediaBuffer]] = None,
+    ):
+        """
+        :param section: 转换后数据对应的section
+        :param chart: 转换后数据对应的图表类型，枚举类型
+        :param data: 存储在.log中的数据
+        :param config: 存储在.log中的配置
+        :param more: 存储在.log中的更多信息
+        :param buffers: 存储于media文件夹中的原始数据，比特流，特别的，对于某些字符串即原始数据的情况，此处为None
+        """
+        self.__data = data
+        self.config = config
+        self.more = more
+        self.buffers = buffers
+        self.section = section
+        self.chart = chart
+        # 默认的reference
+        self.reference = "step"
+        self.step = None
 
     @property
-    def step(self):
-        return self.__step
+    def data(self):
+        """
+        获取数据，如果不确定为何种数据类型，可以使用此属性
+        """
+        return self.__data
 
     @property
-    def settings(self):
-        return self.__settings
+    def float(self):
+        """
+        当确认数据为float时，可以使用此属性明确为float
+        """
+        return self.__data
 
-    @settings.setter
-    def settings(self, value):
-        if self.__settings is None:
-            self.__settings = value
-        else:
-            raise AttributeError("settings can only be set once")
+    @float.setter
+    def float(self, value):
+        if not isinstance(value, (int, float)):
+            # 无法被nan或者inf表示的数据，不允许被设置为float
+            if not (math.isnan(float(value)) or math.isinf(float(value))):
+                raise TypeError(f"Expected float, but got {type(value)}")
+            elif math.isnan(float(value)):
+                swanlog.warning(f"Your are setting a nan to float")
+            else:
+                swanlog.warning(f"Your are setting a inf to float")
+        self.__data = value
 
-    @tag.setter
-    def tag(self, value):
-        if self.__tag is None:
-            # 将tag进行url编码,但是可能已经被编码了，所以需要设置safe="%2F"
-            self.__tag = quote(value, safe="%2F")
-        else:
-            raise AttributeError("tag can only be set once")
+    @property
+    def strings(self):
+        """
+        当确认数据为字符串时，可以使用此属性明确为字符串
+        """
+        return self.__data
 
-    @step.setter
-    def step(self, value):
-        if self.__step is None:
-            self.__step = value
-        else:
-            raise AttributeError("step can only be set once")
+    @strings.setter
+    def strings(self, value):
+        if not isinstance(value, list) or not all(isinstance(i, str) for i in value):
+            raise TypeError(f"Expected list(str), but got {type(value)}")
+        # 不允许设置为空列表，目的是希望在设置时就能保证数据的完整性
+        if not len(value):
+            raise ValueError("Expected list(str) with at least one element")
+        self.__data = value
diff --git a/swanlab/data/modules/chart.py b/swanlab/data/modules/chart.py
deleted file mode 100644
index 23bd6703..00000000
--- a/swanlab/data/modules/chart.py
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2023-12-29 12:25:51
-@File: swanlab/database/modules/chart.py
-@IDE: vscode
-@Description:
-    SwanLab图表选择器，方便用户选择受支持的图表类型
-"""
-
-
-class Chart:
-    # 默认图表
-    default = "line", [float, int]
-    # 折线图
-    line = "line", [float, int]
-    # 图片类型，list代表一步多图
-    image = "image", [list, str]
-    # 音频类型，list代表一步多音频
-    audio = "audio", [list, str]
-    # 文本类型，list代表一步多文本
-    text = "text", [list, str]
-    # 视频类型 list代表一步多视频
-    video = "video", [list, str]
-    # 3D点云类型，list代表一步多3D点云
-    object3d = "object3d", [list, str]
diff --git a/swanlab/data/modules/image.py b/swanlab/data/modules/image.py
deleted file mode 100644
index c346cc35..00000000
--- a/swanlab/data/modules/image.py
+++ /dev/null
@@ -1,339 +0,0 @@
-import numpy as np
-from PIL import Image as PILImage
-from .base import BaseType
-from .utils_modules import BoundingBoxes, ImageMask
-from ._utils import get_file_hash_pil
-from typing import Union, List, Dict, Any
-from io import BytesIO
-import os
-
-
-def is_pytorch_tensor_typename(typename: str) -> bool:
-    return typename.startswith("torch.") and ("Tensor" in typename or "Variable" in typename)
-
-
-def get_full_typename(o: Any) -> Any:
-    """Determine types based on type names.
-
-    Avoids needing to to import (and therefore depend on) PyTorch, TensorFlow, etc.
-    """
-    instance_name = o.__class__.__module__ + "." + o.__class__.__name__
-    if instance_name in ["builtins.module", "__builtin__.module"]:
-        return o.__name__
-    else:
-        return instance_name
-
-
-class Image(BaseType):
-    """Image class constructor
-
-    Parameters
-    ----------
-    data_or_path: (str or numpy.array or PIL.Image or torch.Tensor or matplotlib figure or List["Image"])
-        Path to the image file, numpy array of image data, PIL.Image data, torch.Tensor data or matplotlib figure data.
-    mode: (str)
-        The PIL Mode for a image. Most commom is 'L', 'RGB', 'RGBA'.
-        More information about the mode can be found at https://pillow.readthedocs.io/en/stable/handbook/concepts.html#concept-modes
-    caption: (str)
-        Caption for the image.
-    file_type: (str)
-        File type for the image. It is used to save the image in the specified format. The default is 'png'. The supported file types are ['png', 'jpg', 'jpeg', 'bmp'].
-    size: (int or list or tuple)
-        The size of the image can be controlled in four ways:
-        1. If int type, it represents the maximum side length of the image, that is, the width and height cannot exceed this maximum side length. The image will be scaled proportionally to ensure that the maximum side length does not exceed MAX_DIMENSION.
-        2. If list or tuple type with both specified values, e.g. (500, 500), then the image will be scaled to the specified width and height.
-        3. If list or tuple type with only one specified value and another value as None, e.g. (500, None), it means resize the image to the specified width, and the height is scaled proportionally.
-        4. If it is None, it means no scaling for the image.
-    """
-
-    def __init__(
-        self,
-        data_or_path: Union[str, np.ndarray, PILImage.Image, List["Image"]],
-        mode: str = None,
-        caption: str = None,
-        file_type: str = None,
-        size: Union[int, list, tuple] = None,
-        # boxes: dict = None,
-        # masks: dict = None,
-    ):
-        super().__init__(data_or_path)
-        self.image_data = None
-        self.mode = mode
-        self.caption = self.__convert_caption(caption)
-        self.format = self.__convert_file_type(file_type)
-        self.size = self.__convert_size(size)
-
-        # 提前预处理maplotlib类型
-        if hasattr(self.value, "savefig"):
-            # 如果输入为matplotlib图像
-            self.value = self.__convert_plt_to_image(self.value)
-
-        # TODO: 等前端支持Boxes和Masks后再开启
-
-        # self.boxes = None
-        # self.boxes_total_classes = None
-        # self.masks = None
-        # self.masks_total_classes = None
-        # if boxes:
-        #     self.boxes, self.boxes_total_classes = self.__convert_boxes(boxes)
-        # if masks:
-        #     self.masks, self.masks_total_classes = self.__convert_masks(masks)
-
-    def get_data(self):
-        # 如果传入的是Image类列表
-        if isinstance(self.value, list):
-            return self.get_data_list()
-        # 图像预处理
-        self.__preprocess(self.value)
-        # 判断是否要保存(mode='disabled'时不保存)
-        if not self.settings.should_save:
-            return
-        # 获取图像的hash值
-        hash_name = get_file_hash_pil(self.image_data)[:16]
-        # 设置保存路径, 保存文件名
-        save_dir = os.path.join(self.settings.static_dir, self.tag)
-        save_name = f"image-step{self.step}-{hash_name}.{self.format}"
-        # 如果不存在目录则创建
-        if os.path.exists(save_dir) is False:
-            os.makedirs(save_dir)
-        save_path = os.path.join(save_dir, save_name)
-        # 保存图像到指定目录
-        self.__save(save_path)
-        return save_name
-
-    def expect_types(self, *args, **kwargs) -> list:
-        return ["str", "numpy.array", "PIL.Image.Image", "torch.Tensor"]
-
-    def __convert_caption(self, caption):
-        """将caption转换为字符串"""
-        # 如果类型是字符串，则不做转换
-        if isinstance(caption, str):
-            caption = caption
-        # 如果类型是数字，则转换为字符串
-        elif isinstance(caption, (int, float)):
-            caption = str(caption)
-        # 如果类型是None，则转换为默认字符串
-        elif caption is None:
-            caption = None
-        else:
-            raise TypeError("caption must be a string, int or float.")
-        return caption.strip() if caption else None
-
-    def __convert_boxes(self, boxes):
-        """将boxes转换为Dict[str, BoundingBoxes]类型对象, 并返回该对象和总标签"""
-        # 如果boxes的类型不是字典，则报错
-        if not isinstance(boxes, dict):
-            raise TypeError("swanlab.Image 'boxes' argument must be a dictionary")
-
-        boxes_final: Dict[str, BoundingBoxes] = {}
-        total_classes = {}
-
-        # 对于boxes中的每一个key
-        for key in boxes:
-            box_item = boxes[key]
-            if isinstance(box_item, BoundingBoxes):
-                boxes_final[key] = box_item
-            elif isinstance(box_item, dict):
-                boxes_final[key] = BoundingBoxes(box_item, key)
-            total_classes.update(boxes_final[key]._class_labels)
-
-        return boxes_final, total_classes
-
-    def __convert_masks(self, masks):
-        """将masks转换为Dict[str, ImageMask]类型对象, 并返回该对象和总标签"""
-        # 如果masks的类型不是字典，则报错
-        if not isinstance(masks, dict):
-            raise TypeError("swanlab.Image 'masks' argument must be a dictionary")
-
-        masks_final: Dict[str, ImageMask] = {}
-        total_classes = {}
-
-        # 对于masks中的每一个key
-        for key in masks:
-            mask_item = masks[key]
-            if isinstance(mask_item, ImageMask):
-                masks_final[key] = mask_item
-            elif isinstance(mask_item, dict):
-                masks_final[key] = ImageMask(mask_item, key)
-            total_classes.update(masks_final[key]._class_labels)
-
-        return masks_final, total_classes
-
-    def __convert_file_type(self, file_type):
-        """转换file_type，并检测file_type是否正确"""
-        accepted_formats = ["png", "jpg", "jpeg", "bmp"]
-        if file_type is None:
-            format = "png"
-        else:
-            format = file_type
-
-        if format not in accepted_formats:
-            raise ValueError(f"file_type must be one of {accepted_formats}")
-
-        return format
-
-    def __convert_size(self, size):
-        """将size转换为PIL图像的size"""
-        if size is None:
-            return None
-        if isinstance(size, int):
-            return size
-        if isinstance(size, (list, tuple)):
-            if len(size) == 2:
-                if size[0] is None and size[1] is None:
-                    return None
-                elif size[0] is None:
-                    return (None, int(size[1]))
-                elif size[1] is None:
-                    return (int(size[0]), None)
-                else:
-                    return (int(size[0]), int(size[1]))
-            if len(size) == 1:
-                if size[0] is None:
-                    return None
-                else:
-                    return int(size[0])
-        raise ValueError("size must be an int, list or tuple with 2 or 1 elements")
-
-    def __preprocess(self, data):
-        """将不同类型的输入转换为PIL图像"""
-        if isinstance(data, str):
-            # 如果输入为字符串
-            image = self.__load_image_from_path(data)
-        elif isinstance(data, np.ndarray):
-            # 如果输入为numpy array
-            image = self.__convert_numpy_array_to_image(data)
-        elif isinstance(data, PILImage.Image):
-            # 如果输入为PIL.Image
-            image = data.convert(self.mode)
-        elif is_pytorch_tensor_typename(get_full_typename(data)):
-            # 如果输入为pytorch tensor
-            try:
-                import torchvision
-            except ImportError as e:
-                raise TypeError(
-                    "swanlab.Image requires `torchvision` when process torch.tensor data. Install with 'pip install torchvision'."
-                )
-
-            if hasattr(data, "requires_grad") and data.requires_grad:
-                data = data.detach()
-            if hasattr(data, "detype") and str(data.type) == "torch.uint8":
-                data = data.to(float)
-            data = torchvision.utils.make_grid(data, normalize=True)
-            image = PILImage.fromarray(
-                data.mul(255).clamp(0, 255).byte().permute(1, 2, 0).cpu().numpy(), mode=self.mode
-            )
-        else:
-            # 以上都不是，则报错
-            raise TypeError(
-                "Unsupported image type. Please provide a valid path, numpy array, PIL.Image, torch.Tensor or matplotlib figure."
-            )
-
-        self.image_data = self.__resize(image, self.size)
-
-    def __load_image_from_path(self, path):
-        """判断字符串是否为正确的图像路径，如果是则返回np.ndarray类型对象，如果不是则报错"""
-        try:
-            return PILImage.open(path).convert(self.mode)
-        except Exception as e:
-            raise TypeError(f"Invalid image path: {path}") from e
-
-    def __convert_numpy_array_to_image(self, array):
-        """ """
-        try:
-            if array.ndim == 2 or (array.ndim == 3 and array.shape[2] in [3, 4]):
-                array = np.clip(array, 0, 255).astype(np.uint8)
-                return PILImage.fromarray(array, mode=self.mode)
-            else:
-                raise TypeError("Invalid numpy array: the numpy array must be 2D or 3D with 3 or 4 channels.")
-        except Exception as e:
-            raise TypeError(
-                "Invalid numpy array for the image: the numpy array must be 2D or 3D with 3 or 4 channels."
-            ) from e
-
-    def __convert_plt_to_image(self, plt_obj):
-        """ """
-        try:
-            buf = BytesIO()
-            plt_obj.savefig(buf, format=self.format)  # 将图像保存到BytesIO对象
-            buf.seek(0)  # 移动到缓冲区的开始位置
-            image = PILImage.open(buf).convert(self.mode)  # 使用PIL打开图像
-            buf.close()  # 关闭缓冲区
-            return image
-        except Exception as e:
-            raise TypeError("Invalid matplotlib figure for the image") from e
-
-    def __resize(self, image, size=None):
-        """将图像调整大小"""
-        # 如果size是None, 则返回原图
-        if size is None:
-            return image
-        # 如果size是int类型，且图像的最大边长超过了size，则进行缩放
-        if isinstance(size, int):
-            MAX_DIMENSION = size
-            if max(image.size) > MAX_DIMENSION:
-                image.thumbnail((MAX_DIMENSION, MAX_DIMENSION))
-        # 如果size是list或tuple类型
-        elif isinstance(size, (list, tuple)):
-            # 如果size是两个值的list或tuple，如(500, 500)，则进行缩放
-            if None not in size:
-                image = image.resize(size)
-            else:
-                # 如果size中有一个值为None，且图像对应的边长超过了size中的另一个值，则进行缩放
-                if size[0] is not None:
-                    wpercent = size[0] / float(image.size[0])
-                    hsize = int(float(image.size[1]) * float(wpercent))
-                    image = image.resize((size[0], hsize), PILImage.ANTIALIAS)
-                elif size[1] is not None:
-                    hpercent = size[1] / float(image.size[1])
-                    wsize = int(float(image.size[0]) * float(hpercent))
-                    image = image.resize((wsize, size[1]), PILImage.ANTIALIAS)
-
-        return image
-
-    def __save(self, save_path):
-        """将图像保存到指定路径"""
-        pil_image = self.image_data
-        if not isinstance(pil_image, PILImage.Image):
-            raise TypeError("Invalid image data for the image")
-        try:
-            if self.format == "jpg":
-                pil_image.save(save_path, format="JPEG")
-            else:
-                pil_image.save(save_path, format=self.format)
-
-        except Exception as e:
-            raise TypeError(f"Could not save the image to the path: {save_path}") from e
-
-    def get_more(self, *args, **kwargs) -> dict:
-        """返回config数据"""
-        # 如果传入的是Image类列表
-        if isinstance(self.value, list):
-            return self.get_more_list()
-        else:
-            get_more_dict = {}
-
-            if self.caption is not None:
-                get_more_dict["caption"] = self.caption
-
-            # if self.boxes is not None:
-            #     get_more_dict["boxes"] = self.boxes
-
-            # if self.boxes_total_classes is not None:
-            #     get_more_dict["boxes_total_classes"] = self.boxes_total_classes
-
-            # if self.masks is not None:
-            #     get_more_dict["masks"] = self.masks
-
-            # if self.masks_total_classes is not None:
-            #     get_more_dict["masks_total_classes"] = self.masks_total_classes
-
-            return get_more_dict if get_more_dict else None
-
-    def get_namespace(self, *args, **kwargs) -> str:
-        """设定分组名"""
-        return "Image"
-
-    def get_chart_type(self) -> str:
-        """设定图表类型"""
-        return self.chart.image
diff --git a/swanlab/data/modules/image/__init__.py b/swanlab/data/modules/image/__init__.py
new file mode 100644
index 00000000..2a2e0123
--- /dev/null
+++ b/swanlab/data/modules/image/__init__.py
@@ -0,0 +1,214 @@
+import numpy as np
+from PIL import Image as PILImage
+from matplotlib import pyplot as plt
+from ..base import MediaType, MediaBuffer, DataSuite as D
+from typing import Union, Any
+from io import BytesIO
+
+
+def is_pytorch_tensor_typename(typename: str) -> bool:
+    return typename.startswith("torch.") and ("Tensor" in typename or "Variable" in typename)
+
+
+def get_full_typename(o: Any) -> Any:
+    """Determine types based on type names.
+
+    Avoids needing to import (and therefore depend on) PyTorch, TensorFlow, etc.
+    """
+    instance_name = o.__class__.__module__ + "." + o.__class__.__name__
+    if instance_name in ["builtins.module", "__builtin__.module"]:
+        return o.__name__
+    else:
+        return instance_name
+
+
+def convert_size(size=None):
+    """将size转换为PIL图像的size"""
+    if size is None:
+        return None
+
+    elif isinstance(size, int):
+        return size
+
+    elif isinstance(size, (list, tuple)) and len(size) in [1, 2]:
+        size = tuple(size)
+        width = int(size[0]) if size[0] is not None else None
+        height = int(size[1]) if len(size) > 1 and size[1] is not None else None
+
+        return (width, height) if len(size) == 2 else width
+
+    raise ValueError("swanlab.Image - param `size` must be a list (with 2 or 1 elements) or an int")
+
+
+class Image(MediaType):
+    ACCEPT_FORMAT = ["png", "jpg", "jpeg", "bmp"]
+
+    def __init__(
+        self,
+        data_or_path: Union[str, np.ndarray, PILImage.Image, plt.plot],
+        mode: str = None,
+        caption: str = None,
+        file_type: str = None,
+        size: Union[int, list, tuple] = None,
+    ):
+        """Image class constructor
+
+        Parameters
+        ----------
+        data_or_path: (str or numpy.ndarray or PIL.Image or torch.Tensor or matplotlib figure)
+            Path to the image file, numpy array of image data, PIL.Image data, torch.Tensor data or
+            matplotlib figure data.
+        mode: (str)
+            The PIL Mode for an image. Most common is 'L', 'RGB', 'RGBA'.
+            More information about the mode can be found at
+            https://pillow.readthedocs.io/en/stable/handbook/concepts.html#concept-modes
+        caption: (str)
+            Caption for the image.
+        file_type: (str)
+            File type for the image. It is used to save the image in the specified format. The default is 'png'.
+            The supported file types are ['png', 'jpg', 'jpeg', 'bmp'].
+        size: (int or list or tuple)
+            The size of the image can be controlled in four ways:
+            If int type, it represents the maximum side length of the image, that is,
+            the width and height cannot exceed this maximum side length. The image will be scaled proportionally to
+            ensure that the maximum side length does not exceed MAX_DIMENSION;
+
+            If list or tuple type with both specified values, e.g. (500, 500),
+            then the image will be scaled to the specified width and height;
+
+            If list or tuple type with only one specified value and another value as None, e.g. (500, None),
+            it means resize the image to the specified width, and the height is scaled proportionally.
+
+            If it is None, it means no scaling for the image.
+        """
+
+        super().__init__()
+        self.format = self.__convert_file_type(file_type)
+        self.size = convert_size(size)
+
+        if isinstance(data_or_path, str):
+            # 如果输入为路径字符串
+            try:
+                image_data = PILImage.open(data_or_path).convert(mode)
+            except Exception as e:
+                raise ValueError(f"Invalid image path: {data_or_path}") from e
+        elif isinstance(data_or_path, np.ndarray):
+            # 如果输入为numpy array
+            try:
+                if data_or_path.ndim == 2 or (data_or_path.ndim == 3 and data_or_path.shape[2] in [3, 4]):
+                    image_data = PILImage.fromarray(
+                        np.clip(data_or_path, 0, 255).astype(np.uint8),
+                        mode=mode
+                    )
+                else:
+                    raise TypeError("Invalid numpy array: the numpy array must be 2D or 3D with 3 or 4 channels.")
+            except Exception as e:
+                raise TypeError("Invalid numpy array for the image") from e
+        elif isinstance(data_or_path, PILImage.Image):
+            # 如果输入为PIL.Image
+            image_data = data_or_path.convert(mode)
+        elif is_pytorch_tensor_typename(get_full_typename(data_or_path)):
+            # 如果输入为pytorch tensor
+            try:
+                import torchvision  # noqa
+            except ImportError:
+                raise TypeError(
+                    "swanlab.Image requires `torchvision` when process torch.tensor data. "
+                    "Install with 'pip install torchvision'."
+                )
+
+            if hasattr(data_or_path, "requires_grad") and data_or_path.requires_grad:
+                data_or_path = data_or_path.detach()  # noqa
+            if hasattr(data_or_path, "detype") and str(data_or_path.type) == "torch.uint8":
+                data_or_path = data_or_path.to(float)
+            data_or_path = torchvision.utils.make_grid(data_or_path, normalize=True)
+            image_data = PILImage.fromarray(
+                data_or_path.mul(255).clamp(0, 255).byte().permute(1, 2, 0).cpu().numpy(),
+                mode=mode
+            )
+        elif hasattr(data_or_path, "savefig"):
+            # 如果输入为matplotlib图像
+            try:
+                buf = BytesIO()
+                data_or_path.savefig(buf, format=self.format)  # 将图像保存到BytesIO对象
+                buf.seek(0)  # 移动到缓冲区的开始位置
+                image_data = PILImage.open(buf).convert(mode)  # 使用PIL打开图像
+                buf.close()  # 关闭缓冲区
+            except Exception as e:
+                raise TypeError("Invalid matplotlib figure for the image") from e
+        else:
+            # 以上都不是，则报错
+            raise TypeError(
+                "Unsupported image type. Please provide a valid path, numpy array, PIL.Image, torch."
+                "Tensor or matplotlib figure."
+            )
+
+        self.image_data = self.resize(image_data, self.size)
+        """
+        转换为矩阵后的数据
+        """
+        self.buffer = MediaBuffer()
+        self.image_data.save(self.buffer, format=self.format if self.format != "jpg" else "jpeg")
+        self.caption = D.check_caption(caption)
+
+    def __convert_file_type(self, file_type: str = None):
+        """转换file_type，并检测file_type是否正确"""
+        if file_type is None:
+            file_type = "png"
+
+        if file_type not in self.ACCEPT_FORMAT:
+            raise ValueError(f"file_type must be one of {self.ACCEPT_FORMAT}")
+
+        return file_type
+
+    def resize(self, image: PILImage.Image, size=None) -> PILImage.Image:
+        """将图像调整大小"""
+        if size is None:
+            self.size = image.size
+            return image
+
+        elif isinstance(size, int):
+            if max(image.size) > size:
+                image.thumbnail((size, size))
+            return image
+
+        elif isinstance(size, (list, tuple)):
+            size = tuple(size)
+            if all(size):
+                return image.resize(size)
+            if size[0] is not None:
+                w_percent = size[0] / float(image.size[0])
+                hsize = int(float(image.size[1]) * w_percent)
+                return image.resize((size[0], hsize))
+            if size[1] is not None:
+                h_percent = size[1] / float(image.size[1])
+                w_size = int(float(image.size[0]) * h_percent)
+                return image.resize((w_size, size[1]))
+
+        raise ValueError("swanlab.Image - param `size` must be a list (with 2 or 1 elements) or an int")
+
+    @property
+    def image_size(self):
+        """
+        得到图像的大小
+        """
+        return self.image_data.size
+
+    # ---------------------------------- 覆写方法 ----------------------------------
+
+    def parse(self):
+        # 文件名称
+        hash_name = D.get_hash_by_pil(self.image_data)[:16]
+        save_name = f"image-step{self.step}-{hash_name}.{self.format}"
+        return save_name, self.buffer
+
+    def get_more(self):
+        """返回more数据"""
+        return {"caption": self.caption} if self.caption is not None else None
+
+    def get_section(self) -> str:
+        """设定分组名"""
+        return "Image"
+
+    def get_chart(self):
+        return self.Chart.IMAGE
diff --git a/swanlab/data/modules/utils_modules/bounding_boxes.py b/swanlab/data/modules/image/bounding_boxes.py
similarity index 100%
rename from swanlab/data/modules/utils_modules/bounding_boxes.py
rename to swanlab/data/modules/image/bounding_boxes.py
diff --git a/swanlab/data/modules/utils_modules/image_mask.py b/swanlab/data/modules/image/image_mask.py
similarity index 100%
rename from swanlab/data/modules/utils_modules/image_mask.py
rename to swanlab/data/modules/image/image_mask.py
diff --git a/swanlab/data/modules/line/__init__.py b/swanlab/data/modules/line/__init__.py
new file mode 100644
index 00000000..af7af586
--- /dev/null
+++ b/swanlab/data/modules/line/__init__.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 02:32
+@File: __init__.py.py
+@IDE: pycharm
+@Description:
+    浮点数模块
+    解析浮点数
+    1. 正常情况下返回浮点数, None
+    2. 如果传入的数为不可转变为浮点数的类型，抛出DataTypeError异常
+    3. 如果传入NaN，返回NaN字符串，None
+    4. 如果传入Infinity，返回INF字符串，None
+"""
+from swanlab.error import DataTypeError
+from typing import Protocol, runtime_checkable
+from ..base import BaseType, DataSuite as D
+
+
+@runtime_checkable
+class FloatConvertible(Protocol):
+    """
+    实现了__float__方法的类
+    """
+
+    def __float__(self) -> float: ...
+
+
+class Line(BaseType):
+    nan = "NaN"
+    inf = "INF"
+
+    def __init__(self, value):
+        super().__init__()
+        self.value = value
+
+    def parse(self):
+        # 如果是nan
+        try:
+            t = float(self.value)
+            if D.is_nan(t):
+                return self.nan, None
+            # 如果是inf
+            if D.is_inf(t):
+                return self.inf, None
+            return t, None
+        except (ValueError, TypeError):
+            raise DataTypeError('float', type(self.value).__name__)
+
+    def get_chart(self):
+        return self.Chart.LINE
diff --git a/swanlab/data/modules/object_3d.py b/swanlab/data/modules/object_3d.py
deleted file mode 100644
index 7ad50a0a..00000000
--- a/swanlab/data/modules/object_3d.py
+++ /dev/null
@@ -1,191 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Author: nexisato
-Date: 2024-02-21 17:52:22
-FilePath: /SwanLab/swanlab/data/modules/object_3d.py
-Description:
- 3D Point Cloud data parsing
-"""
-
-from .base import BaseType
-import numpy as np
-from typing import Union, ClassVar, Set, List, Optional
-from ._utils import get_file_hash_numpy_array, get_file_hash_path
-import os
-import json
-import shutil
-from io import BytesIO
-
-# 格式化输出 json
-import codecs
-
-
-class Object3D(BaseType):
-    """Object 3D class constructor
-
-    Parameters
-    ----------
-    data_or_path: numpy.array, string, io
-        Path to an object3d format file or numpy array of object3d or Bytes.IO.
-
-        numpy.array: 3D point cloud data, shape (N, 3), (N, 4) or (N, 6).
-        (N, 3) :  N  * (x, y, z) coordinates
-        (N, 4) :  N  * (x, y, z, c) coordinates, where c in range [1, 14]
-        (N, 6) :  N  * (x, y, z, r, g, b) coordinates
-    caption: str
-        caption associated with the object3d for display
-    """
-
-    SUPPORTED_TYPES: ClassVar[Set[str]] = {
-        "obj",
-        "gltf",
-        "glb",
-        "babylon",
-        "stl",
-        "pts.json",
-    }
-
-    def __init__(
-        self,
-        data_or_path: Union[str, "np.ndarray", "BytesIO", List["Object3D"]],
-        caption: Optional[str] = None,
-    ):
-        super().__init__(data_or_path)
-        self.object3d_data = None
-        self.caption = self.__convert_caption(caption)
-        self.extension = None
-
-    def get_data(self):
-        # 如果传入的是Object3D类列表
-        if isinstance(self.value, list):
-            return self.get_data_list()
-
-        self.object3d_data = self.__preprocess(self.value)
-
-        # 判断是否要保存(mode='disabled'时不保存)
-        if not self.settings.should_save:
-            return
-
-        # 根据不同的输入类型进行不同的哈希校验
-        hash_name = (
-            get_file_hash_numpy_array(self.object3d_data)[:16]
-            if isinstance(self.object3d_data, np.ndarray)
-            else get_file_hash_path(self.object3d_data)[:16]
-        )
-
-        save_dir = os.path.join(self.settings.static_dir, self.tag)
-        save_name = save_name = f"obj3d-step{self.step}-{hash_name}.{self.format}"
-        # 如果不存在目录则创建
-        if os.path.exists(save_dir) is False:
-            os.makedirs(save_dir)
-        save_path = os.path.join(save_dir, save_name)
-
-        self.__save(save_path)
-        return save_name
-
-    def __preprocess(self, data_or_path):
-        """根据输入不同的输入类型进行不同处理"""
-        # 如果类型为 str，进行文件后缀格式检查
-        if isinstance(data_or_path, str):
-            extension = None
-            for SUPPORTED_TYPE in Object3D.SUPPORTED_TYPES:
-                if data_or_path.endswith(SUPPORTED_TYPE):
-                    extension = SUPPORTED_TYPE
-                    break
-            if not extension:
-                raise TypeError(
-                    "File '"
-                    + data_or_path
-                    + "' is not compatible with Object3D: supported types are: "
-                    + ", ".join(Object3D.SUPPORTED_TYPES)
-                )
-            self.extension = extension
-            return data_or_path
-        # 如果类型为 io.BytesIO 二进制流，直接返回
-        elif isinstance(data_or_path, BytesIO):
-            self.extension = "pts.json"
-            return data_or_path
-
-        # 如果类型为 numpy.array，进行numpy格式检查
-        elif isinstance(data_or_path, np.ndarray):
-            if len(data_or_path.shape) != 2 or data_or_path.shape[1] not in {3, 4, 6}:
-                raise TypeError(
-                    """
-                    The shape of the numpy array must be one of either:
-                        (N, 3) :  N  * (x, y, z) coordinates
-                        (N, 4) :  N  * (x, y, z, c) coordinates, where c in range [1, 14]
-                        (N, 6) :  N  * (x, y, z, r, g, b) coordinates
-                    """
-                )
-            self.extension = "pts.json"
-            return data_or_path
-        else:
-            raise TypeError("swanlab.Object3D accepts a file path or numpy like data as input")
-
-    def __convert_caption(self, caption):
-        """将caption转换为字符串"""
-        # 如果类型是字符串，则不做转换
-        if isinstance(caption, str):
-            caption = caption
-        # 如果类型是数字，则转换为字符串
-        elif isinstance(caption, (int, float)):
-            caption = str(caption)
-        # 如果类型是None，则转换为默认字符串
-        elif caption is None:
-            caption = None
-        else:
-            raise TypeError("caption must be a string, int or float.")
-        return caption.strip() if caption else None
-
-    def __save_numpy(self, save_path):
-        """保存 numpy.array 格式的 3D点云资源文件 .pts.json 到指定路径"""
-        try:
-            list_data = self.object3d_data.tolist()
-            with codecs.open(save_path, "w", encoding="utf-8") as fp:
-                json.dump(
-                    list_data,
-                    fp,
-                    separators=(",", ":"),
-                    sort_keys=True,
-                    indent=4,
-                )
-        except Exception as e:
-            raise TypeError(f"Could not save the 3D point cloud data to the path: {save_path}") from e
-
-    def __save(self, save_path):
-        """
-        保存 3D点云资源文件到指定路径
-        """
-        if isinstance(self.object3d_data, str):
-            shutil.copy(self.object3d_data, save_path)
-        elif isinstance(self.object3d_data, BytesIO):
-            with open(save_path, "wb") as f:
-                f.write(self.object3d_data.read())
-        elif isinstance(self.object3d_data, np.ndarray):
-            self.__save_numpy(save_path)
-
-    def get_more(self, *args, **kwargs) -> dict:
-        """返回config数据"""
-        # 如果传入的是Objet3d类列表
-        if isinstance(self.value, list):
-            return self.get_more_list()
-        else:
-            return (
-                {
-                    "caption": self.caption,
-                }
-                if self.caption is not None
-                else None
-            )
-
-    def expect_types(self, *args, **kwargs) -> list:
-        """返回支持的文件类型"""
-        return ["str", "numpy.array", "io"]
-
-    def get_namespace(self, *args, **kwargs) -> str:
-        """设定分组名"""
-        return "Object3D"
-
-    def get_chart_type(self) -> str:
-        """设定图表类型"""
-        return self.chart.object3d
diff --git a/swanlab/data/modules/text.py b/swanlab/data/modules/text.py
deleted file mode 100644
index 0b9a7d68..00000000
--- a/swanlab/data/modules/text.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# -*- coding: utf-8 -*-
-from .base import BaseType
-from typing import Union, List
-
-
-class Text(BaseType):
-    """Text class constructor
-
-    Parameters
-    ----------
-    data: str, float, int
-        text data.
-    caption: str
-        caption for the data, it will be displayed in the dashboard.
-        e.g. swanlab.Text("Hello World", caption="This is a caption for the text data.")
-    """
-
-    def __init__(self, data: Union[str, List["Text"]], caption: str = None):
-        super().__init__(data)
-        self.text_data = None
-        self.caption = self.__convert_caption(caption)
-
-    def get_data(self):
-        # 如果传入的是Text类列表
-        if isinstance(self.value, list):
-            return self.get_data_list()
-
-        # 预处理文本数据
-        self.__preprocess(self.value)
-
-        # 判断是否要保存(mode='disabled'时不保存)
-        if not self.settings.should_save:
-            return
-
-        return self.text_data
-
-    def expect_types(self, *args, **kwargs) -> list:
-        return ["str", "int", "float"]
-
-    def __preprocess(self, data):
-        """
-        根据不同的输入类型进行不同处理
-        """
-        if isinstance(data, str):
-            self.text_data = data
-        elif isinstance(data, (int, float)):
-            self.text_data = str(data)
-        else:
-            raise TypeError("data must be a string, int or float.")
-
-    def __convert_caption(self, caption):
-        """将caption转换为字符串"""
-        # 如果类型是字符串，则不做转换
-        if isinstance(caption, str):
-            caption = caption
-        # 如果类型是数字，则转换为字符串
-        elif isinstance(caption, (int, float)):
-            caption = str(caption)
-        # 如果类型是None，则转换为默认字符串
-        elif caption is None:
-            caption = None
-        else:
-            raise TypeError("caption must be a string, int or float.")
-        return caption.strip() if caption else None
-
-    def get_more(self, *args, **kwargs) -> dict:
-        """返回config数据"""
-        # 如果传入的是Text类列表
-        if isinstance(self.value, list):
-            return self.get_more_list()
-        else:
-            return (
-                {
-                    "caption": self.caption,
-                }
-                if self.caption is not None
-                else None
-            )
-
-    def get_namespace(self, *args, **kwargs) -> str:
-        """设定分组名"""
-        return "Text"
-
-    def get_chart_type(self) -> str:
-        """设定图表类型"""
-        return self.chart.text
diff --git a/swanlab/data/modules/text/__init__.py b/swanlab/data/modules/text/__init__.py
new file mode 100644
index 00000000..36309f7c
--- /dev/null
+++ b/swanlab/data/modules/text/__init__.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 02:35
+@File: __init__.py
+@IDE: pycharm
+@Description:
+    文本模块
+"""
+from ..base import MediaType, DataSuite as D
+from typing import Union
+
+
+class Text(MediaType):
+    """Text class constructor
+
+    Parameters
+    ----------
+    data: str, float, int
+        text data.
+    caption: str
+        caption for the data, it will be displayed in the dashboard.
+        e.g. swanlab.Text("Hello World", caption="This is a caption for the text data.")
+    """
+
+    def __init__(self, data: Union[str, int, float], caption: str = None):
+        super().__init__()
+        # 处理文本数据
+
+        if isinstance(data, str):
+            self.text_data = data
+        elif isinstance(data, (int, float)):
+            self.text_data = str(data)
+        else:
+            raise TypeError("data must be a string, int or float.")
+
+        # 处理caption
+        self.caption = D.check_caption(caption)
+
+    # ---------------------------------- 覆写 ----------------------------------
+
+    def parse(self):
+        return self.text_data, None
+
+    def get_more(self, *args, **kwargs) -> dict:
+        """返回config数据"""
+        return {"caption": self.caption} if self.caption is not None else None
+
+    def get_section(self, *args, **kwargs) -> str:
+        """设定分组名"""
+        return "Text"
+
+    def get_chart(self):
+        """设定图表类型"""
+        return self.Chart.TEXT
diff --git a/swanlab/data/modules/utils_modules/__init__.py b/swanlab/data/modules/utils_modules/__init__.py
deleted file mode 100644
index d96c3198..00000000
--- a/swanlab/data/modules/utils_modules/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .bounding_boxes import BoundingBoxes
-from .image_mask import ImageMask
diff --git a/swanlab/data/modules/video.py b/swanlab/data/modules/video.py
deleted file mode 100644
index 40a876f4..00000000
--- a/swanlab/data/modules/video.py
+++ /dev/null
@@ -1,211 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-02-20 17:16:44
-@File: swanlab\data\modules\video.py
-@IDE: vscode
-@Description:
-     视频数据解析
-"""
-from .base import BaseType
-from ._utils import get_file_hash_numpy_array, get_file_hash_path
-import os
-from typing import Union, List
-import logging
-import numpy as np
-from io import BytesIO
-from typing import Optional
-import shutil
-
-
-class Video(BaseType):
-    """Video class constructor
-
-    Parameters
-    ----------
-    data_or_path: numpy array, string, io
-            Video can be initialized with a path to a file or an io object.
-            The format must be "gif", "mp4", "webm" or "ogg".
-            The format must be specified with the format argument.
-            Video can be initialized with a numpy tensor.
-            The numpy tensor must be either 4 dimensional or 5 dimensional.
-            Channels should be (time, channel, height, width) or
-            (batch, time, channel, height width)
-            The time axis is measured in frames
-    caption: str
-        caption associated with the video for display
-    fps: int
-        frames per second for video. Default is 4.
-    format: str
-        format of video, necessary if initializing with path or io object.
-    """
-
-    EXTS = ("gif", "mp4", "webm", "ogg")
-    width: Optional[int]
-    height: Optional[int]
-
-    def __init__(
-        self,
-        data_or_path: Union[str, "np.ndarray", "BytesIO", List["Video"]],
-        caption: Optional[str] = None,
-        fps: int = 4,
-        format: Optional[str] = None,
-    ):
-        super().__init__(data_or_path)
-        self.caption = self.__convert_caption(caption)
-        self.fps = fps
-        self.format = format
-        self.height = None
-        self.width = None
-        self.channels = None
-        # self.format的默认值为mp4
-        self.format = format or "mp4"
-        # 如果self.format不在Video.EXTS中，则报错
-        if self.format not in Video.EXTS:
-            raise ValueError("swanlab.Video 'format' paramters's format range is ({}) .".format(", ".join(Video.EXTS)))
-
-    def get_data(self, *args, **kwargs):
-        # 如果传入的是Video类列表
-        if isinstance(self.value, list):
-            return self.get_data_list()
-        # 视频预处理
-        self.video_data = self.__preprocess(self.value)
-
-        # 判断是否要保存(mode='disabled'时不保存)
-        if not self.settings.should_save:
-            return
-
-        # 获取视频hash值
-        hash_name = (
-            get_file_hash_numpy_array(self.video_data)[:16]
-            if isinstance(self.video_data, np.ndarray)
-            else get_file_hash_path(self.video_data)[:16]
-        )
-        # 设置视频保存路径, 保存文件名
-        save_dir = os.path.join(self.settings.static_dir, self.tag)
-        save_name = f"text-step{self.step}-{hash_name}.{self.format}"
-        if not os.path.exists(save_dir):
-            os.mkdir(save_dir)
-        save_path = os.path.join(save_dir, save_name)
-
-        # 保存视频数据到指定目录
-        self.__save(save_path)
-        return save_name
-
-    def expect_types(self, *args, **kwargs) -> list:
-        return ["str", "np.ndarray", "BytesIO"]
-
-    def __convert_caption(self, caption):
-        """将caption转换为字符串"""
-        # 如果类型是字符串，则不做转换
-        if isinstance(caption, str):
-            caption = caption
-        # 如果类型是数字，则转换为字符串
-        elif isinstance(caption, (int, float)):
-            caption = str(caption)
-        # 如果类型是None，则转换为默认字符串
-        elif caption is None:
-            caption = None
-        else:
-            raise TypeError("caption must be a string, int or float.")
-        return caption.strip() if caption else None
-
-    def __preprocess(self, data):
-        """将不同类型的输入转换为np.ndarray类型"""
-        if isinstance(data, (str, BytesIO)):
-            # 如果输入为字符串或二进制数据流
-            return data
-
-        elif isinstance(data, np.ndarray):
-            # 如果输入为numpy array
-            data = self.__numpy_preprocess(data)
-            return data
-
-        else:
-            # 以上都不是，则报错
-            raise ValueError("swanlab.Video accepts a file path or numpy like data as input")
-
-    def __numpy_preprocess(self, video: "np.ndarray") -> "np.ndarray":
-        """This logic was mostly taken from tensorboardX."""
-        if video.ndim < 4:
-            raise ValueError("Video must be atleast 4 dimensions: time, channels, height, width")
-        if video.ndim == 4:
-            video = video.reshape(1, *video.shape)
-        b, t, c, h, w = video.shape
-
-        if video.dtype != np.uint8:
-            logging.warning("Converting video data to uint8")
-            video = video.astype(np.uint8)
-
-        def is_power2(num: int) -> bool:
-            return num != 0 and ((num & (num - 1)) == 0)
-
-        # pad to nearest power of 2, all at once
-        if not is_power2(video.shape[0]):
-            len_addition = int(2 ** video.shape[0].bit_length() - video.shape[0])
-            video = np.concatenate((video, np.zeros(shape=(len_addition, t, c, h, w))), axis=0)
-
-        n_rows = 2 ** ((b.bit_length() - 1) // 2)
-        n_cols = video.shape[0] // n_rows
-
-        video = np.reshape(video, newshape=(n_rows, n_cols, t, c, h, w))
-        video = np.transpose(video, axes=(2, 0, 4, 1, 5, 3))
-        video = np.reshape(video, newshape=(t, n_rows * h, n_cols * w, c))
-
-        _, self.height, self.width, self.channels = video.shape
-        return video
-
-    def __save(self, save_path):
-        """将视频保存到指定路径"""
-        # 如果data_or_path是路径, 则直接复制文件到目标文件夹
-        if isinstance(self.video_data, str):
-            shutil.copyfile(self.video_data, save_path)
-
-        # 如果data_or_path是字节流, 则直接写入文件到目标文件夹
-        elif isinstance(self.video_data, BytesIO):
-            with open(save_path, "wb") as f:
-                f.write(self.video_data.read())
-
-        # 如果data_or_path是np.ndarray，则使用moviepy库保存视频
-        elif isinstance(self.video_data, np.ndarray):
-            try:
-                import moviepy
-            except ImportError as e:
-                raise TypeError(
-                    "swanlab.Video requires moviepy when process numpy data. Install with 'pip install moviepy'."
-                )
-
-            from moviepy.editor import ImageSequenceClip
-
-            write_video_data_bgr = self.video_data
-            # OpenCV 默认读取为BGR，转化为RGB
-            write_video_data_rgb = [frame[:, :, ::-1] for frame in write_video_data_bgr]
-            # 读取视频帧
-            clip = ImageSequenceClip(list(write_video_data_rgb), fps=self.fps)
-            try:
-                clip.write_videofile(save_path, codec="libx264")
-                clip.close()
-            except Exception as e:
-                raise TypeError(f"Could not save the video to the path: {save_path}") from e
-
-    def get_more(self, *args, **kwargs) -> dict:
-        """返回config数据"""
-        # 如果传入的是Video类列表
-        if isinstance(self.value, list):
-            return self.get_more_list()
-        else:
-            return (
-                {
-                    "caption": self.caption,
-                }
-                if self.caption is not None
-                else None
-            )
-
-    def get_namespace(self, *args, **kwargs) -> str:
-        """设定分组名"""
-        return "Video"
-
-    def get_chart_type(self, *args, **kwargs) -> str:
-        """设定图表类型"""
-        return self.chart.video
diff --git a/swanlab/data/modules/wrapper.py b/swanlab/data/modules/wrapper.py
new file mode 100644
index 00000000..39180e9c
--- /dev/null
+++ b/swanlab/data/modules/wrapper.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 15:39
+@File: wrapper.py
+@IDE: pycharm
+@Description:
+    包装器
+"""
+from .base import BaseType, MediaType, ParseResult
+from typing import Union, List, Optional
+from swanlab.error import DataTypeError
+from .line import Line
+
+
+class ErrorInfo:
+    """
+    DataWrapper转换时的错误信息
+    """
+
+    def __init__(
+        self,
+        expected: Optional[str],
+        got: Optional[str],
+        chart: Optional[BaseType.Chart],
+        duplicated: bool = False
+    ):
+        """
+        :param expected: 期望的数据类型
+        :param got: 实际的数据类型
+        :param chart: 当前错误数据对应的图表类型
+        :param duplicated: 是否是重复错误,如果为是，expected和got和chart都为None
+        """
+        self.expected = expected if not duplicated else None
+        self.got = got if not duplicated else None
+        self.chart = chart if not duplicated else None
+        self.__duplicated = duplicated
+
+    def dict(self):
+        return {"expected": self.expected, "got": self.got}
+
+    @property
+    def duplicated(self) -> bool:
+        """
+        是否是重复错误，重复错误时，got和expected为None
+        """
+        return self.__duplicated
+
+
+class DataWrapper:
+    """
+    数据包装器，负责解析数据，返回相应的数据类型
+
+    一个数据包装器对应swanlab.log字典内的一个key的值
+    """
+
+    def __init__(self, key: str, data: Union[List[Line], List[MediaType]]):
+        """
+        :param key: key名称，为编码前的名称
+        :param data: log的数据
+        """
+        self.__key = key
+        self.__data = data
+        self.__error = None
+        self.__result: Optional[ParseResult] = None
+        # 保证data内所有数据类型相同，否则返回TypeError
+        t = self.__data[0].__class__
+        for i in self.__data:
+            if i.__class__ != t:
+                raise TypeError(f"All data in DataWrapper must be the same type, got {t} and {i.__class__}")
+
+    @property
+    def is_line(self) -> bool:
+        """
+        是否是Line类型
+        """
+        return isinstance(self.__data[0], Line) and len(self.__data) == 1
+
+    @property
+    def type(self):
+        """
+        数据类型
+        """
+        return self.__data[0].__class__
+
+    @property
+    def parsed(self) -> bool:
+        """
+        是否已经解析过数据
+        """
+        return self.__result is not None or self.__error is not None
+
+    @property
+    def error(self) -> Optional[ErrorInfo]:
+        """
+        解析时候的错误信息
+        """
+        return self.__error
+
+    def parse(self, **kwargs) -> Optional[ParseResult]:
+        """
+        将数据解析成对应的数据类型
+        **kwargs: 在解析数据类型之前，需要注入的信息
+        """
+        if self.parsed:
+            return self.__result
+        result = ParseResult()
+        result.step = kwargs["step"]  # 挂载step
+        d = self.__data[0]
+        result.section = d.get_section()
+        result.chart = d.get_chart()
+        # ---------------------------------- 特别处理Line类型 ----------------------------------
+
+        # [Line]
+        if self.type == Line:
+            if len(self.__data) > 1:
+                self.__error = ErrorInfo("float", "list(Line)", result.chart)
+            else:
+                d.inject(**kwargs)
+                try:
+                    result.float, _ = d.parse()
+                except DataTypeError as e:
+                    self.__error = ErrorInfo(e.expected, e.got, result.chart)
+            self.__result = result
+            return self.__result
+
+        # ---------------------------------- 处理其他类型 ----------------------------------
+        # [MediaType]
+        data, buffers, more, config = [], [], [], []
+        for i in self.__data:
+            try:
+                i.inject(**kwargs)
+                d, r = i.parse()
+            except DataTypeError as e:
+                self.__error = ErrorInfo(e.expected, e.got, result.chart)
+                return None
+            data.append(d)
+            buffers.append(r)
+            more.append(i.get_more())
+            config.append(i.get_config())
+        result.strings = data
+        # 过滤掉空列表
+        result.buffers = self.__filter_list(buffers)
+        result.more = self.__filter_list(more)
+        result.config = self.__filter_list(config)
+        self.__result = result
+        return self.__result
+
+    @staticmethod
+    def __filter_list(li: List):
+        """
+        如果li长度大于0且如果l内部不全是None，返回l，否则返回None
+        """
+        if len(li) > 0 and any(i is not None for i in li):
+            return li
+        return None
+
+    @classmethod
+    def create_duplicate_error(cls) -> ErrorInfo:
+        """
+        快捷创建一个重复错误
+        """
+        return ErrorInfo(None, None, None, duplicated=True)
diff --git a/swanlab/data/run/callback.py b/swanlab/data/run/callback.py
index bb5307b1..c8e3ff4c 100644
--- a/swanlab/data/run/callback.py
+++ b/swanlab/data/run/callback.py
@@ -7,14 +7,15 @@
 @Description:
     回调函数注册抽象类
 """
-from typing import Union, Optional, Callable, Dict
+from typing import Union, Optional, Callable, Dict, List
 from abc import ABC, abstractmethod
 from swanlab.data.settings import SwanDataSettings
-from swanlab.data.modules import DataType
+from swanlab.data.modules import ChartType, ErrorInfo, MediaBuffer
 from swanlab.log import swanlog
 from swanlab.utils.font import FONT
 from swanlab.env import is_windows
 from swanlab.package import get_package_version
+from urllib.parse import quote
 import atexit
 import sys
 import os
@@ -29,10 +30,9 @@ def __init__(
         self,
         key: str,
         namespace: str,
-        data_type: str,
-        chart_type: str,
-        sort: int,
-        error: Optional[Dict] = None,
+        chart: ChartType,
+        sort: Optional[int] = None,
+        error: Optional[ErrorInfo] = None,
         reference: Optional[str] = None,
         config: Optional[Dict] = None,
     ):
@@ -44,11 +44,7 @@ def __init__(
         """
         列的命名空间
         """
-        self.data_type = data_type
-        """
-        列的数据类型
-        """
-        self.chart_type = chart_type
+        self.chart = chart
         """
         列的图表类型
         """
@@ -56,7 +52,7 @@ def __init__(
         """
         列的类型错误信息
         """
-        self.reference = reference if reference is not None else "step"
+        self.reference = reference
         """
         列的参考对象
         """
@@ -69,29 +65,50 @@ def __init__(
         列的额外配置信息
         """
 
+    @property
+    def got(self):
+        """
+        传入的错误类型，如果列出错，返回错误类型，如果没出错，`暂时`返回None
+        """
+        if self.error is None:
+            return None
+        return self.error.got
+
+    @property
+    def expected(self):
+        """
+        期望的类型，如果列出错，返回期望的类型，如果没出错，`暂时`返回None
+        """
+        if self.error is None:
+            return None
+        return self.error.expected
+
 
 class MetricInfo:
     """
     指标信息，当新的指标被log时，会生成这个对象
     """
+    __SUMMARY_NAME = "_summary.json"
 
     def __init__(
         self,
         key: str,
         column_info: ColumnInfo,
+        error: Optional[ErrorInfo],
         metric: Union[Dict, None] = None,
         summary: Union[Dict, None] = None,
-        data_type: Union[float, DataType] = None,
         step: int = None,
         epoch: int = None,
-        metric_path: str = None,
-        summary_path: str = None,
-        static_dir: str = None,
-        error: bool = True,
+        logdir: str = None,
+        metric_file_name: str = None,
+        media_dir: str = None,
+        buffers: List[MediaBuffer] = None,
     ):
-        self.key = key
+        self.__error = error
+
+        self.key = quote(key, safe="")
         """
-        指标的key名称
+        指标的key名称，被quote编码
         """
         self.column_info = column_info
         """
@@ -105,10 +122,6 @@ def __init__(
         """
         摘要信息，error时为None
         """
-        self.data_type = data_type
-        """
-        当前指标的数据类型，error时为None
-        """
         self.step = step
         """
         当前指标的步数，error时为None
@@ -117,22 +130,66 @@ def __init__(
         """
         当前指标对应本地的行数，error时为None
         """
-        self.metric_path = metric_path
+        self.metric_path = None if self.error else os.path.join(logdir, self.key, metric_file_name)
         """
         指标文件的路径，error时为None
         """
-        self.summary_path = summary_path
+        self.summary_path = None if self.error else os.path.join(logdir, self.key, self.__SUMMARY_NAME)
         """
         摘要文件的路径，error时为None
         """
-        self.static_dir = static_dir
+        self.media_dir = media_dir
         """
-        静态文件的根文件夹，error时为None
+        静态文件的根文件夹
         """
-        self.error = error
+        self.buffers = buffers
+        """
+        需要上传的媒体数据，比特流，error时为None，如果上传为非媒体类型（或Text类型），也为None
+        """
+        # 写入文件名称，对应上传时的文件名称
+        if self.buffers is not None:
+            for i, buffer in enumerate(self.buffers):
+                buffer.file_name = "{}/{}".format(self.key, metric["data"][i])
+
+    @property
+    def error(self) -> bool:
+        """
+        这条指标信息是否有错误，错误分几种：
+            1. 列错误，列一开始就出现问题
+            2. 重复错误
+            3. 指标错误
+        """
+        return self.error_info is not None or self.column_error_info is not None
+
+    @property
+    def column_error_info(self) -> Optional[ErrorInfo]:
+        """
+        列错误信息
+        """
+        return self.column_info.error
+
+    @property
+    def error_info(self) -> Optional[ErrorInfo]:
+        """
+        指标错误信息
+        """
+        return self.__error
+
+    @property
+    def duplicated_error(self) -> bool:
+        """
+        是否是重复的指标
+        """
+        return self.__error and self.__error.duplicated
+
+    @property
+    def data(self) -> Union[Dict, None]:
         """
-        指标是否有错误
+        指标数据的data字段
         """
+        if self.error:
+            return None
+        return self.metric["data"]
 
 
 class U:
diff --git a/swanlab/data/run/exp.py b/swanlab/data/run/exp.py
index 4d713bd0..efc4f38e 100644
--- a/swanlab/data/run/exp.py
+++ b/swanlab/data/run/exp.py
@@ -1,14 +1,11 @@
-import json
 from swanlab.data.settings import SwanDataSettings
-from swanlab.data.modules import BaseType, DataType
+from swanlab.data.modules import DataWrapper, Line, ErrorInfo
 from swanlab.log import swanlog
-from typing import Dict, Union, Optional
+from typing import Dict, Optional
 from swanlab.utils import create_time
-from swanlab.utils.file import check_tag_format
 from .callback import MetricInfo, ColumnInfo
 from .operator import SwanLabRunOperator
-from urllib.parse import quote
-import os
+import json
 import math
 
 
@@ -30,102 +27,99 @@ def __init__(self, settings: SwanDataSettings, operator: SwanLabRunOperator) ->
         """
         self.settings = settings
         # 当前实验的所有tag数据字段
-        self.tags: Dict[str, SwanLabTag] = {}
+        self.keys: Dict[str, SwanLabKey] = {}
         self.__operator = operator
-        """回调函数"""
 
-    def add(self, key: str, data: DataType, step: int = None) -> MetricInfo:
+    def add(self, key: str, data: DataWrapper, step: int = None) -> MetricInfo:
         """记录一条新的tag数据
 
         Parameters
         ----------
         key : str
-            tag名称，需要检查数据类型
-        data : Union[int, float, BaseType]
-            tag数据，可以是浮点型，也可以是SwanLab定义的数据类型，具体与添加的图表类型有关系
-            如果data是int或者float，添加图表时自动添加默认折线图，如果是BaseType，添加图表时选择对应的图表
-            需要检查数据类型
+            key名称
+        data : DataWrapper
+            包装后的数据，用于数据解析
 
         step : int, optional
             步数，如果不传则默认当前步数为'已添加数据数量+1'
             在log函数中已经做了处理，此处不需要考虑数值类型等情况
         """
-        tag = check_tag_format(key, auto_cut=True)
-        if key != tag:
-            # 超过255字符，截断
-            swanlog.warning(f"Tag {key} is too long, cut to 255 characters.")
-
-        # 如果是swanlab自定义的数据类型，注入settings，方便在类内部使用
-        if isinstance(data, BaseType):
-            data.settings = self.settings
         # 判断tag是否存在，如果不存在则创建tag
-        tag_obj: SwanLabTag = self.tags.get(tag, None)
-        """
-        数据库创建字段将在chart创建完成后进行
-        无论格式是否正确，都会创建chart，但是如果格式不正确，不会写入日志
-        """
-        if tag_obj is None:
+        key_obj: SwanLabKey = self.keys.get(key, None)
+
+        # ---------------------------------- 包装器解析 ----------------------------------
+
+        if step is not None and not isinstance(step, int):
+            swanlog.warning(f"Step {step} is not int, SwanLab will set it automatically.")
+            step = None
+        if key_obj is None:
+            step = 0 if step is None or not isinstance(step, int) else step
+        else:
+            step = len(key_obj.steps) if step is None else step
+            if step in key_obj.steps:
+                swanlog.warning(f"Step {step} on key {key} already exists, ignored.")
+                return MetricInfo(key, key_obj.column_info, DataWrapper.create_duplicate_error())
+        data.parse(step=step, settings=self.settings, key=key)
+
+        # ---------------------------------- 图表创建 ----------------------------------
+
+        if key_obj is None:
             # 将此tag对象添加到实验列表中
-            tag_obj = SwanLabTag(tag, self.settings.log_dir)
-            self.tags[tag] = tag_obj
-            """
-            由于添加图表的同时会尝试转换data的类型，但这在注入step之前
-            所以此处需要手动注入依赖
-            关于step，如果step格式不正确，会在tag_obj.add的时候被拦截，此处如果格式不正确直接设置为0即可
-            """
-            if isinstance(data, BaseType):
-                data.step = 0 if step is None or not isinstance(step, int) else step
-                data.tag = tag_obj.tag
-            column_info = tag_obj.create_chart(tag, data)
-            self.warn_type_error(tag)
+            key_obj = SwanLabKey(key, self.settings.log_dir)
+            self.keys[key] = key_obj
+            # 新建图表，完成数据格式校验
+            column_info = key_obj.create_chart(key, data)
+            self.warn_type_error(key)
             # 创建新列，生成回调
             self.__operator.on_column_create(column_info)
 
         # 检查tag创建时图表是否创建成功，如果失败则也没有写入数据的必要了，直接退出
-        if not tag_obj.is_chart_valid:
-            self.warn_chart_error(tag)
-            return MetricInfo(key, tag_obj.column_info)
-        key_info = tag_obj.add(data, step)
-        key_info.static_dir = self.settings.static_dir
+        if not key_obj.is_chart_valid:
+            self.warn_chart_error(key)
+            # 这条指标没有被解析，所以也没有必要标注这条只指标是否错误
+            return MetricInfo(key, key_obj.column_info, error=None)
+        key_info = key_obj.add(data)
+        key_info.buffers = data.parse().buffers
+        key_info.media_dir = self.settings.media_dir
         return key_info
 
-    def warn_type_error(self, tag: str):
+    def warn_type_error(self, key: str):
         """警告类型错误
-        执行此方法时需保证tag已经存在
+        执行此方法时需保证key已经存在
         """
-        tag_obj = self.tags[tag]
-        class_name = tag_obj.now_data_type
-        excepted = tag_obj.expect_data_types
+        tag_obj = self.keys[key]
+        class_name = tag_obj.column_info.got
+        expected = tag_obj.column_info.expected
         if tag_obj.is_chart_valid:
             return
         if class_name == "list":
-            swanlog.error(f"Data type error, tag: {tag}, there is element of invalid data type in the list.")
+            swanlog.error(f"Data type error, key: {key}, there is element of invalid data type in the list.")
         else:
-            swanlog.error(f"Data type error, tag: {tag}, data type: {class_name}, excepted: {excepted}")
+            swanlog.error(f"Data type error, key: {key}, data type: {class_name}, expected: {expected}")
 
-    def warn_chart_error(self, tag: str):
+    def warn_chart_error(self, key: str):
         """
         警告图表创建错误
-        执行此方法时需保证tag已经存在
+        执行此方法时需保证key已经存在
         """
-        tag_obj = self.tags[tag]
+        tag_obj = self.keys[key]
         if tag_obj.is_chart_valid:
             return
-        class_name = tag_obj.now_data_type
+        class_name = tag_obj.column_info.got
         if class_name == "list":
             swanlog.warning(
-                f"Chart '{tag}' creation failed. "
-                f"Reason: The data type in list of the tag '{tag}' is not as expected, please check the data type."
+                f"Chart '{key}' creation failed. "
+                f"Reason: The data type in list of the key '{key}' is not as expected, please check the data type."
             )
         else:
             swanlog.warning(
-                f"Chart '{tag}' creation failed. "
-                f"Reason: The expected value type for the chart '{tag}' is one of int,"
+                f"Chart '{key}' creation failed. "
+                f"Reason: The expected value type for the chart '{key}' is one of int,"
                 f"float or BaseType, but the input type is {class_name}."
             )
 
 
-class SwanLabTag:
+class SwanLabKey:
     """
     运行时每个tag的配置，用于记录一些信息
     包括每个tag专属的图表配置
@@ -134,66 +128,47 @@ class SwanLabTag:
     # 每__slice_size个tag数据保存为一个文件
     __slice_size = 1000
 
-    def __init__(self, tag: str, log_dir: str) -> None:
+    def __init__(self, key: str, log_dir: str) -> None:
         """
         初始化tag对象
 
         Parameters
         ----------
-        tag : str
-            tag名称
+        key : str
+            列名称
         log_dir : str
             log文件夹路径
         """
-        self.tag = tag
+        self.key = key
         self.__steps = set()
         """
         此tag已经包含的steps步骤
         """
         self.__log_dir = log_dir
-        """存储文件夹路径"""
-        self.data_types = [float, int]
-        """默认数据类型，如果tag数据为BaseType的子类，则使用其规定的数据类型"""
-        self._summary = {}
+        """swanlab 存储文件夹路径"""
+        self.__summary = {}
         """数据概要总结"""
-        self.__data = self.__new_tags()
+        self.__collection = self.__new_metric_collection()
         """当前tag的数据"""
-        self.__error = None
-        """此tag在自动生成chart的时候的错误信息"""
-        self.data_type = None
+        self.chart = None
         """当前tag的数据类型，如果是BaseType类型，则为BaseType的小写类名，否则为default"""
         self.__column_info = None
 
-    @property
-    def now_data_type(self) -> Optional[str]:
-        """
-        当is_chart_valid为False时，返回当前数据的类型
-        这指的是用户传入的数据类型，而不是转换后的数据类型
-        """
-        if self.__error is not None:
-            return self.__error.get("data_class")
-        return None
-
-    @property
-    def expect_data_types(self) -> Optional[list]:
-        """
-        当is_chart_valid为False时，返回期望的数据类型
-        如果为True，则返回None
-        """
-        if self.__error is not None:
-            return self.__error.get("excepted")
-        return None
-
     @property
     def sum(self):
         """当前tag的数据总数"""
         return len(self.__steps)
 
     @property
-    def column_info(self):
+    def column_info(self) -> Optional[ColumnInfo]:
         """获取当前tag对应的ColumnInfo"""
         return self.__column_info
 
+    @property
+    def steps(self):
+        """获取当前tag的所有步数"""
+        return self.__steps
+
     @property
     def chart_created(self):
         """判断当前tag是否已经创建了图表"""
@@ -202,23 +177,10 @@ def chart_created(self):
     @property
     def is_chart_valid(self) -> bool:
         """判断当前tag对应的自动创建图表是否成功
-        成功则返回False，失败则返回True
-        为True则一切正常
-        为False则tag对应的路径不存在
         """
-        return self.__error is None
+        return self.column_info.error is None
 
-    @staticmethod
-    def __is_nan(data):
-        """判断data是否为nan"""
-        return isinstance(data, (int, float)) and math.isnan(data)
-
-    @staticmethod
-    def __is_inf(data):
-        """判断data是否为inf"""
-        return isinstance(data, (int, float)) and math.isinf(data)
-
-    def add(self, data: DataType, step: int = None) -> MetricInfo:
+    def add(self, data: DataWrapper) -> MetricInfo:
         """添加一个数据，在内部完成数据类型转换
         如果转换失败，打印警告并退出
         并且添加数据，当前的数据保存是直接保存，后面会改成缓存形式
@@ -227,8 +189,6 @@ def add(self, data: DataType, step: int = None) -> MetricInfo:
         ----------
         data : DataType
             待添加的数据
-        step : int, optional
-            步数，如果不传则默认当前步数为'已添加数据数量+1'
 
         Returns
         -------
@@ -237,132 +197,76 @@ def add(self, data: DataType, step: int = None) -> MetricInfo:
         ok
             是否添加成功，如果当前step已经存在，或者类型转换失败，返回False
         """
-        if step is not None and not isinstance(step, int):
-            swanlog.warning(f"Step {step} is not int, SwanLab will set it automatically.")
-            step = None
-        if step is None:
-            step = len(self.__steps)
-        if step in self.__steps:
-            swanlog.warning(f"Step {step} on tag {self.tag} already exists, ignored.")
-            return MetricInfo(self.tag, self.__column_info)
-        more = None if not isinstance(data, BaseType) else data.get_more()
-        try:
-            data = self.try_convert_after_add_chart(data, step)
-        except ValueError:
+        result = data.parse()
+        if data.error is not None:
             swanlog.warning(
-                f"Log failed. Reason: Data {data} on tag '{self.tag}' (step {step}) cannot be converted .It should be "
-                f"an int, float, or a DataType, but it is {type(data)}, please check the data type."
+                f"Log failed. Reason: Data on key '{self.key}' (step {result.step}) cannot be converted ."
+                f"It should be {data.error.expected}, but it is {data.error.got}, please check the data type."
             )
-            return MetricInfo(self.tag, self.__column_info)
-        is_nan = self.__is_nan(data)
-        is_inf = self.__is_inf(data)
-        # 更新数据概要
-        if not is_nan and not is_inf:
-            if self._summary.get("max") is None or data > self._summary["max"]:
-                self._summary["max"] = data
-                self._summary["max_step"] = step
-            if self._summary.get("min") is None or data < self._summary["min"]:
-                self._summary["min"] = data
-                self._summary["min_step"] = step
-        self._summary["num"] = self._summary.get("num", 0) + 1
-        self.__steps.add(step)
-        swanlog.debug(f"Add data, tag: {self.tag}, step: {step}, data: {data}")
-        if len(self.__data["data"]) >= self.__slice_size:
-            self.__data = self.__new_tags()
-
-        data = data if not is_nan else "NaN"
-        data = data if not is_inf else "INF"
-
-        new_data = self.__new_tag(step, data, more=more)
-        self.__data["data"].append(new_data)
+            return MetricInfo(self.key, self.__column_info, data.error)
+
+        # 如果为Line且为NaN或者INF，不更新summary
+        r = result.strings or result.float
+
+        if not data.type == Line or r not in [Line.nan, Line.inf]:
+            if self.__summary.get("max") is None or r > self.__summary["max"]:
+                self.__summary["max"] = r
+                self.__summary["max_step"] = result.step
+            if self.__summary.get("min") is None or r < self.__summary["min"]:
+                self.__summary["min"] = r
+                self.__summary["min_step"] = result.step
+        self.__summary["num"] = self.__summary.get("num", 0) + 1
+        self.__steps.add(result.step)
+        swanlog.debug(f"Add data, key: {self.key}, step: {result.step}, data: {r}")
+        if len(self.__collection["data"]) >= self.__slice_size:
+            self.__collection = self.__new_metric_collection()
+
+        new_data = self.__new_metric(result.step, r, more=result.more)
+        self.__collection["data"].append(new_data)
         epoch = len(self.__steps)
         mu = math.ceil(epoch / self.__slice_size)
-        file_path = os.path.join(self.save_path, str(mu * self.__slice_size) + ".log")
         return MetricInfo(
-            self.tag,
+            self.key,
             self.__column_info,
-            json.loads(json.dumps(new_data)),
-            json.loads(json.dumps(self._summary)),
-            self.data_type,
-            step,
-            epoch,
-            metric_path=file_path,
-            summary_path=os.path.join(self.save_path, "_summary.json"),
-            error=False,
+            error=None,
+            epoch=epoch,
+            step=result.step,
+            logdir=self.__log_dir,
+            metric=json.loads(json.dumps(new_data)),
+            summary=json.loads(json.dumps(self.__summary)),
+            metric_file_name=str(mu * self.__slice_size) + ".log",
+            buffers=result.buffers,
         )
 
-    @property
-    def save_path(self):
-        """获取当前tag的保存路径
-
-        Returns
-        -------
-        str
-            保存路径
-        """
-        path = os.path.join(self.__log_dir, quote(self.tag, safe=""))
-        return path
-
-    def create_chart(self, tag: str, data: DataType) -> ColumnInfo:
+    def create_chart(self, key: str, data: DataWrapper) -> ColumnInfo:
         """在第一次添加tag的时候，自动创建图表和namespaces，同时写入tag和数据库信息，将创建的信息保存到数据库中
         此方法只能执行一次
         具体步骤是：
-        1. 创建tag字段
+        1. 创建key字段
         2. 如果不是baseType类型，
-        :returns tag, chart_type, error
-
-        WARNING 返回位置需与回调函数入参位置一致
         """
         if self.chart_created:
-            raise ValueError(f"Chart {tag} has been created, cannot create again.")
-        # 如果是非BaseType类型，写入默认命名空间，否则写入BaseType指定的命名空间
-        sort = 0
-        if not isinstance(data, BaseType):
-            namespace, chart_type, reference, config = "default", "default", "step", None
-            data_type = "default"
-        else:
-            namespace, types, reference, config = data.__next__()
-            chart_type, self.data_types = types
-            data_type = data.__class__.__name__.lower()
+            raise ValueError(f"Chart {key} has been created, cannot create again.")
+        result = data.parse()
+
         # 对于namespace，如果tag的名称存在斜杠，则使用斜杠前的部分作为namespace的名称
-        if "/" in tag and tag[0] != "/":
-            namespace = tag.split("/")[0]
-            sort = None
-        """
-        接下来判断tag格式的正确性，判断完毕后往source中添加一条tag记录
-        在此函数中，只判断tag的格式是否正确，不记录数据
-        在逻辑上只有第一次会检查tag的正确性，也就是说前端的error错误只有在第一次添加tag的时候才有可能出现
-        如果第一次添加成功，后续出现错误，只会在添加的时候warning一下然后丢弃这个错误
-        如果第一次添加失败，后续都不会再添加此数据，因此不会出现错误
-        """
-        # 如果data不是期望的data_types中的类型，尝试转换为这两个类型中的一个（优先转换为第一个）
-        # 如果data是BaseType类型，会在try_convert中完成转换，此处不需要管
-        error = None
-        if not isinstance(data, tuple(self.data_types)):
-            try:
-                data = self.try_convert(data)
-            except ValueError:
-                # 此时代表数据异常，拿到data的__class__.__name__，生成error并保存
-                if isinstance(data, BaseType):
-                    class_name = data.value.__class__.__name__
-                    excepted = data.expect_types()
-                else:
-                    class_name = data.__class__.__name__
-                    excepted = [i.__name__ for i in self.data_types]
-                error = {"data_class": class_name, "excepted": excepted}
-        if self.__is_nan(data):
-            error = {"data_class": "NaN", "excepted": [i.__name__ for i in self.data_types]}
-        if self.__is_inf(data):
-            error = {"data_class": "INF", "excepted": [i.__name__ for i in self.data_types]}
-
-        column_info = ColumnInfo(tag, namespace, data_type, chart_type, sort, error, reference, config)
-        self.__error = error
-        self.data_type = data_type
+        if "/" in key and key[0] != "/":
+            result.section = key.split("/")[0]
+
+        column_info = ColumnInfo(
+            key=key,
+            namespace=result.section,
+            chart=result.chart,
+            error=data.error,
+            reference=result.reference,
+            config=result.config,
+        )
+        self.chart = result.chart.value
         self.__column_info = column_info
         return column_info
 
     @staticmethod
-    def __new_tag(index, data, more: dict = None) -> dict:
+    def __new_metric(index, data, more: dict = None) -> dict:
         """创建一个新的data数据，实际上是一个字典，包含一些默认信息
 
         Parameters
@@ -389,8 +293,8 @@ def __new_tag(index, data, more: dict = None) -> dict:
             }
 
     @staticmethod
-    def __new_tags() -> dict:
-        """创建一个新的tag data数据集合
+    def __new_metric_collection() -> dict:
+        """创建一个新的key data数据集合
 
         Returns
         -------
@@ -403,40 +307,3 @@ def __new_tags() -> dict:
             "update_time": time,
             "data": [],
         }
-
-    def try_convert(self, value: DataType):
-        # 如果当前data已经是data_types中的类型，直接返回
-        if isinstance(value, tuple(self.data_types)):
-            return value
-        if isinstance(value, BaseType):
-            value = value.convert
-        for data_type in self.data_types:
-            try:
-                if type(value) in self.data_types:
-                    return value
-                converted_value = data_type(value)
-                return converted_value
-            except (ValueError, TypeError):
-                # 如果转换失败，继续尝试下一种类型
-                continue
-        # 如果所有类型都尝试过仍然失败，则抛出异常
-        raise ValueError(f"Unable to convert {value} to any of the specified types.")
-
-    def try_convert_after_add_chart(self, data, step):
-        """尝试将data转换为data_types中的类型，如果转换失败，返回None
-        如果所有类型都尝试过仍然失败，则抛出异常
-        调用这个函数代表用户的图表已经创建并且传入了与之前不同的数据类型
-        """
-        try:
-            if isinstance(data, BaseType):
-                # 注入内容
-                if data.step is None:
-                    data.step = step
-                if data.tag is None:
-                    data.tag = self.tag
-            return self.try_convert(data)
-        except ValueError:
-            raise ValueError(
-                f"Unable to convert {data} to any of the specified types. This Error means that you have changed the "
-                f"data type of the chart, please check the data type of the chart."
-            )
diff --git a/swanlab/data/run/main.py b/swanlab/data/run/main.py
index d009d897..27e25d0a 100644
--- a/swanlab/data/run/main.py
+++ b/swanlab/data/run/main.py
@@ -9,7 +9,7 @@
 """
 from ..settings import SwanDataSettings
 from swanlab.log import swanlog
-from swanlab.data.modules import BaseType
+from swanlab.data.modules import MediaType, DataWrapper, FloatConvertible, Line
 from .config import SwanLabConfig
 import random
 from enum import Enum
@@ -17,7 +17,8 @@
 from datetime import datetime
 from typing import Callable, Optional, Dict, MutableMapping
 from .operator import SwanLabRunOperator
-from swanlab.env import get_mode, SwanLabMode
+from swanlab.env import get_mode
+from ...utils.file import check_key_format
 
 
 class SwanLabRunState(Enum):
@@ -84,7 +85,7 @@ def __init__(
         # ---------------------------------- 初始化类内参数 ----------------------------------
         self.__project_name = project_name
         # 生成一个唯一的id，随机生成一个8位的16进制字符串，小写
-        _id = hex(random.randint(0, 2**32 - 1))[2:].zfill(8)
+        _id = hex(random.randint(0, 2 ** 32 - 1))[2:].zfill(8)
         timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
         self.__run_id = "run-{}-{}".format(timestamp, _id)
         # 操作员初始化
@@ -246,15 +247,32 @@ def log(self, data: dict, step: int = None):
 
         log_return = {}
         # 遍历data，记录data
-        for key in data:
-            # 遍历字典的key，记录到本地文件中
-            d = data[key]
-            # 如果d的数据类型是list，且里面的数据全部为DataType类型，则需要转换一下
-            if isinstance(d, list) and all([isinstance(i, BaseType) for i in d]) and len(d) > 0:
-                # 将d作为输入，构造一个与d相同类型的实例
-                d = d[0].__class__(d)
+        for k, v in data.items():
+            _k = k
+            k = check_key_format(k, auto_cut=True)
+            if k != _k:
+                # 超过255字符，截断
+                swanlog.warning(f"Key {_k} is too long, cut to 255 characters.")
+            # ---------------------------------- 包装数据 ----------------------------------
+            # 输入为可转换为float的数据类型
+            if isinstance(v, (int, float, FloatConvertible)):
+                v = DataWrapper(k, [Line(v)])
+            # 为Line类型或者MediaType类型
+            elif isinstance(v, (Line, MediaType)):
+                v = DataWrapper(k, [v])
+            # 为List[MediaType]或者List[Line]类型，且长度大于0，且所有元素类型相同
+            elif (
+                isinstance(v, list)
+                and len(v) > 0
+                and all([isinstance(i, (Line, MediaType)) for i in v])
+                and all([i.__class__ == v[0].__class__ for i in v])
+            ):
+                v = DataWrapper(k, v)
+            else:
+                # 其余情况被当作是非法的数据类型，交给Line处理
+                v = DataWrapper(k, [Line(v)])
             # 数据类型的检查将在创建chart配置的时候完成，因为数据类型错误并不会影响实验进行
-            metric_info = self.__exp.add(key=key, data=d, step=step)
+            metric_info = self.__exp.add(key=k, data=v, step=step)
             self.__operator.on_metric_create(metric_info)
             log_return[metric_info.key] = metric_info
 
diff --git a/swanlab/data/sdk.py b/swanlab/data/sdk.py
index 9d048cd7..837b7c2a 100644
--- a/swanlab/data/sdk.py
+++ b/swanlab/data/sdk.py
@@ -8,7 +8,7 @@
     在此处封装swanlab在日志记录模式下的各种接口
 """
 import os
-from typing import Optional, Union, Dict, Tuple
+from typing import Optional, Union, Dict, Tuple, Literal
 from .modules import DataType
 from .run import (
     SwanLabRunState,
@@ -26,14 +26,6 @@
 from swanlab.db import GlomCallback
 from swanlab.package import version_limit
 
-"""
-Allows users to record experiment configurations through swanlab.config.
-Before calling the init() function, config cannot be read or written, even if it is a SwanLabConfig object.
-After calling the init() function, swanlab.config is equivalent to run.config.
-Configuration information synchronization is achieved through class variables.
-When the run object is initialized, it will operate on the SwanLabConfig object to write the configuration.
-"""
-
 
 def _check_proj_name(name: str) -> str:
     """检查项目名称是否合法，如果不合法则抛出ValueError异常
@@ -89,7 +81,7 @@ def init(
     config: Union[dict, str] = None,
     logdir: str = None,
     suffix: Union[str, None, bool] = "default",
-    mode: str = None,
+    mode: Literal["disabled", "cloud", "local"] = None,
     load: str = None,
     **kwargs,
 ) -> SwanLabRun:
diff --git a/swanlab/data/settings.py b/swanlab/data/settings.py
index 580aa576..9b678651 100644
--- a/swanlab/data/settings.py
+++ b/swanlab/data/settings.py
@@ -127,8 +127,8 @@ def console_dir(self) -> str:
         return os.path.join(self.run_dir, "console")
 
     @property
-    def static_dir(self) -> str:
-        """静态资源路径"""
+    def media_dir(self) -> str:
+        """媒体文件路径"""
         path = os.path.join(self.run_dir, "media")
         self.mkdir(path)
         return path
diff --git a/swanlab/db/callback.py b/swanlab/db/callback.py
index b78f7a2f..adf176f0 100644
--- a/swanlab/db/callback.py
+++ b/swanlab/db/callback.py
@@ -149,7 +149,7 @@ def on_column_create(self, column_info: ColumnInfo):
         chart = Chart.create(
             column_info.key,
             experiment_id=self.exp,
-            type=column_info.data_type,
+            type=column_info.chart.value.chart_type,
             reference=column_info.reference,
             config=column_info.config,
         )
@@ -167,10 +167,13 @@ def on_column_create(self, column_info: ColumnInfo):
         tag: Tag = Tag.create(
             experiment_id=self.exp.id,
             name=column_info.key,
-            type=column_info.data_type,
+            type=column_info.chart.value.chart_type,
         )
         # 添加一条source记录
-        Source.create(tag_id=tag.id, chart_id=chart.id, error=column_info.error)
+        error = None
+        if column_info.error is not None:
+            error = {"data_class": column_info.error.got, "excepted": column_info.error.expected}
+        Source.create(tag_id=tag.id, chart_id=chart.id, error=error)
         # 新建多实验对比图表数据
         try:
             add_multi_chart(tag_id=tag.id, chart_id=chart.id)
diff --git a/swanlab/error.py b/swanlab/error.py
index 81c0cc7d..3304f59b 100644
--- a/swanlab/error.py
+++ b/swanlab/error.py
@@ -83,3 +83,18 @@ def __init__(self, *args):
         super().__init__(*args)
         self.log_level = "warning"
         self.message = "network error, swanlab will resume uploads when the network improves"
+
+
+class DataTypeError(Exception):
+    """数据类型错误，此时数据类型不符合预期
+    """
+
+    def __init__(self, expected: str, got: str):
+        """
+        :param expected: 期望的数据类型
+        :param got: 实际的数据类型
+        """
+        super().__init__(f"expected: {expected}, got: {got}")
+        self.expected = expected
+        self.got = got
+
diff --git a/swanlab/server/controller/experiment.py b/swanlab/server/controller/experiment.py
index 5dcc93fb..0275767c 100644
--- a/swanlab/server/controller/experiment.py
+++ b/swanlab/server/controller/experiment.py
@@ -205,8 +205,8 @@ def get_tag_data(experiment_id: int, tag: str) -> dict:
     if os.path.exists(summary_path):
         with open(summary_path, "r") as f:
             summary = ujson.load(f)
-            max_value = summary["max"]
-            min_value = summary["min"]
+            max_value = summary.get("max", None)
+            min_value = summary.get("min", None)
     else:
         # COMPAT 如果_summary文件不存在，手动获取最大值和最小值
         warn = f"Summary file of tag '{tag}' not found, SwanLab will automatically get the maximum and minimum values."
diff --git a/swanlab/utils/file.py b/swanlab/utils/file.py
index 98251210..4182cf63 100644
--- a/swanlab/utils/file.py
+++ b/swanlab/utils/file.py
@@ -243,9 +243,9 @@ def check_desc_format(description: str, auto_cut: bool = True):
     return _auto_cut("description", description, max_length, auto_cut)
 
 
-def check_tag_format(key: str, auto_cut=True) -> str:
-    """检查tag字符串格式
-    不能超过255个字符，可以包含任何字符
+def check_key_format(key: str, auto_cut=True) -> str:
+    """检查key字符串格式
+    不能超过255个字符，可以包含任何字符，不允许.和/以及空格开头
 
     Parameters
     ----------
@@ -272,7 +272,11 @@ def check_tag_format(key: str, auto_cut=True) -> str:
     max_len = 255
     if not isinstance(key, str):
         raise TypeError(f"tag: {key} is not a string")
+    # 删除头尾空格
+    key = key.lstrip().rstrip()
     if not check_string(key):
         raise ValueError(f"tag: {key} is an empty string")
+    if key.startswith((".", "/")):
+        raise ValueError(f"tag: {key} can't start with '.' or '/' and blank space")
     # 检查长度
     return _auto_cut("tag", key, max_len, auto_cut)


Test patch:
diff --git a/test/unit/api/pytest_http.py b/test/unit/api/pytest_http.py
index b882bc91..631a66a2 100644
--- a/test/unit/api/pytest_http.py
+++ b/test/unit/api/pytest_http.py
@@ -13,6 +13,7 @@
 import nanoid
 from swanlab.api.http import create_http, HTTP, CosClient
 from swanlab.api.auth.login import login_by_key
+from swanlab.data.modules import MediaBuffer
 from tutils import KEY, TEMP_PATH
 
 alphabet = "abcdefghijklmnopqrstuvwxyz"
@@ -47,9 +48,13 @@ def test_cos_ok(self):
         assert self.http.cos is not None
 
     def test_cos_upload(self):
-        self.http.upload("/key", self.file_path)
+        # 新建一个文件对象
+        buffer = MediaBuffer()
+        buffer.write(b"test")
+        buffer.file_name = "test"
+        self.http.upload(buffer)
         # 开发版本设置的过期时间为3s，等待过期
         time.sleep(3)
         # 重新上传，测试刷新
         assert self.http.cos.should_refresh is True
-        self.http.upload("/key", self.file_path)
+        self.http.upload(buffer)
diff --git a/test/unit/data/modules/audio/pytest_audio.py b/test/unit/data/modules/audio/pytest_audio.py
new file mode 100644
index 00000000..1ef19dc0
--- /dev/null
+++ b/test/unit/data/modules/audio/pytest_audio.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 15:43
+@File: pytest_audio.py
+@IDE: pycharm
+@Description:
+    测试音频模块
+"""
+import os.path
+
+from swanlab.data.modules import Audio
+from tutils import TEMP_PATH
+from nanoid import generate
+import numpy as np
+import soundfile as sf
+import pytest
+
+
+def test_audio_ok():
+    # ---------------------------------- numpy输入 ----------------------------------
+
+    np.random.randn(2, 100000)
+    mock = np.random.randn(2, 100000)
+    audio = Audio(data_or_path=mock, sample_rate=44100)
+    data, buffer = audio.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".wav")
+    assert buffer is not None
+    assert audio.get_more() is None
+    assert audio.get_config() is None
+
+    # ---------------------------------- 路径输入 ----------------------------------
+
+    mock = np.random.randn(2, 100000)
+    path = os.path.join(TEMP_PATH, f"{generate()}.wav")
+    # 写入文件
+    sample_rate = 44200
+    sf.write(path, mock.T, sample_rate)
+    audio = Audio(data_or_path=path, sample_rate=sample_rate)
+    data, buffer = audio.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".wav")
+    assert buffer is not None
+
+
+def test_audio_caption():
+    # ---------------------------------- numpy输入 ----------------------------------
+
+    np.random.randn(2, 100000)
+    mock = np.random.randn(2, 100000)
+    audio = Audio(data_or_path=mock, sample_rate=44100, caption="test")
+    data, buffer = audio.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".wav")
+    assert buffer is not None
+    assert audio.get_more()["caption"] == "test"
+    assert audio.get_config() is None
+
+    # ---------------------------------- 路径输入 ----------------------------------
+
+    mock = np.random.randn(2, 100000)
+    path = os.path.join(TEMP_PATH, f"{generate()}.wav")
+    # 写入文件
+    sample_rate = 44200
+    sf.write(path, mock.T, sample_rate)
+    audio = Audio(data_or_path=path, sample_rate=sample_rate, caption="test")
+    data, buffer = audio.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".wav")
+    assert buffer is not None
+    assert audio.get_more()["caption"] == "test"
+    assert audio.get_config() is None
+
+
+@pytest.mark.parametrize("dtype", Audio.SF_SUPPORT_DTYPE)
+def test_audio_numpy(dtype):
+    """
+    测试不同的numpy输入情况
+    """
+    mock = np.random.randn(2, 100000).astype(dtype)
+    Audio(data_or_path=mock, sample_rate=44100)
+    # 单通道
+    mock = np.random.randn(100000).astype(dtype)
+    Audio(data_or_path=mock, sample_rate=44100)
+
+
+def test_audio_fail():
+    # 错误的路径
+    with pytest.raises(ValueError):
+        Audio(data_or_path="not_exist.wav", sample_rate=44100)
+    # 不是音频
+    path = os.path.join(TEMP_PATH, f"{generate()}.wav")
+    with open(path, "w") as f:
+        f.write("hello")
+    with pytest.raises(ValueError):
+        Audio(data_or_path=path, sample_rate=44100)
+
+    # ---------------------------------- 矩阵错误 ----------------------------------
+
+    # 传入矩阵时没有传入采样率
+    mock = np.random.randn(2, 100000)
+    with pytest.raises(TypeError):
+        Audio(data_or_path=mock, sample_rate=None)  # noqa
+
+    # 错误的矩阵类型
+    mock = np.random.randn(2, 100000).astype(np.int8)
+    with pytest.raises(TypeError):
+        Audio(data_or_path=mock, sample_rate=44100)
+
+    # 错误的矩阵形状
+    mock = np.random.randn(3, 100000)
+    with pytest.raises(TypeError):
+        Audio(data_or_path=mock, sample_rate=44100)
diff --git a/test/unit/data/modules/image/pytest_image.py b/test/unit/data/modules/image/pytest_image.py
new file mode 100644
index 00000000..6ed35d7d
--- /dev/null
+++ b/test/unit/data/modules/image/pytest_image.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 15:43
+@File: pytest_image.py
+@IDE: pycharm
+@Description:
+    测试图片模块
+"""
+import os.path
+
+from swanlab.data.modules import Image
+from tutils import TEMP_PATH
+from nanoid import generate
+import numpy as np
+import pytest
+import torch
+from PIL import Image as PILImage
+from matplotlib import pyplot as plt
+
+
+def test_image_ok():
+    # ---------------------------------- numpy输入 ----------------------------------
+
+    np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
+    mock = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
+    image = Image(data_or_path=mock)
+    data, buffer = image.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+    assert image.get_more() is None
+    assert image.get_config() is None
+
+    # ---------------------------------- pil输入 ----------------------------------
+
+    mock = PILImage.fromarray(np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8))
+    image = Image(data_or_path=mock)
+    data, buffer = image.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+    assert image.get_more() is None
+    assert image.get_config() is None
+
+    # ---------------------------------- 路径输入 ----------------------------------
+    mock = PILImage.fromarray(np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8))
+    path = os.path.join(TEMP_PATH, f"{generate()}.png")
+    # 写入文件
+    mock.save(path)
+    audio = Image(data_or_path=path)
+    data, buffer = audio.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+
+    # ---------------------------------- pytorch tensor输入 ----------------------------------
+    mock = torch.randn(4, 3, 256, 256)
+    image = Image(data_or_path=mock)
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+    assert image.get_more() is None
+    assert image.get_config() is None
+
+    # ---------------------------------- plt输入 ----------------------------------
+    x = [1, 2, 3]
+    y = [2, 3, 5]
+    plt.plot(x, y)
+    image = Image(data_or_path=plt)
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+    assert image.get_more() is None
+    assert image.get_config() is None
+
+
+def test_image_caption():
+    # ---------------------------------- numpy输入 ----------------------------------
+
+    np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
+    mock = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
+    image = Image(data_or_path=mock, caption="test")
+    data, buffer = image.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+    assert image.get_more()["caption"] == "test"
+    assert image.get_config() is None
+
+    # ---------------------------------- pil输入 ----------------------------------
+
+    mock = PILImage.fromarray(np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8))
+    image = Image(data_or_path=mock, caption="test")
+    data, buffer = image.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+    assert image.get_more()["caption"] == "test"
+    assert image.get_config() is None
+
+    # ---------------------------------- 路径输入 ----------------------------------
+    mock = PILImage.fromarray(np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8))
+    path = os.path.join(TEMP_PATH, f"{generate()}.png")
+    # 写入文件
+    mock.save(path)
+    audio = Image(data_or_path=path, caption="test")
+    data, buffer = audio.parse()
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+    assert image.get_more()["caption"] == "test"
+    assert image.get_config() is None
+
+    # ---------------------------------- pytorch tensor输入 ----------------------------------
+    mock = torch.randn(4, 3, 256, 256)
+    image = Image(data_or_path=mock, caption="test")
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+    assert image.get_more()["caption"] == "test"
+    assert image.get_config() is None
+
+    # ---------------------------------- plt输入 ----------------------------------
+    x = [1, 2, 3]
+    y = [2, 3, 5]
+    plt.plot(x, y)
+    image = Image(data_or_path=plt, caption="test")
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(".png")
+    assert buffer is not None
+    assert image.get_more()["caption"] == "test"
+    assert image.get_config() is None
+
+
+def test_image_fail():
+    # 错误的路径
+    with pytest.raises(ValueError):
+        Image(data_or_path="not_exist.png")
+    # 不是图片
+    path = os.path.join(TEMP_PATH, "test.png")
+    with open(path, "w") as f:
+        f.write("hello")
+    with pytest.raises(ValueError):
+        Image(data_or_path=path)
+
+    # ---------------------------------- 矩阵错误 ----------------------------------
+
+    # 通道数错误
+    mock = np.random.randint(low=0, high=256, size=(100, 100, 2), dtype=np.uint8)
+    with pytest.raises(TypeError):
+        Image(data_or_path=mock)
+
+    mock = np.random.randint(low=0, high=256, size=(100, 100, 5), dtype=np.uint8)
+    with pytest.raises(TypeError):
+        Image(data_or_path=mock)
+
+    # 错误的矩阵形状
+    mock = np.random.randint(low=0, high=256, size=(3, 100, 100), dtype=np.uint8)
+    with pytest.raises(TypeError):
+        Image(data_or_path=mock)
+
+    mock = np.random.randint(low=0, high=256, size=(1, 100, 100, 3), dtype=np.uint8)
+    with pytest.raises(TypeError):
+        Image(data_or_path=mock)
+
+    # ---------------------------------- 文件类型错误 ----------------------------------
+    mock = np.random.randint(low=0, high=256, size=(100, 100, 2), dtype=np.uint8)
+    with pytest.raises(ValueError):
+        Image(mock, file_type="svg")
+
+    # ---------------------------------- size错误 -------------------------------------
+    mock = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
+    with pytest.raises(ValueError):
+        Image(mock, size=(100, 100, 100))
+    with pytest.raises(ValueError):
+        Image(mock, size="hello")  # noqa
+
+
+@pytest.mark.parametrize("file_type", Image.ACCEPT_FORMAT)
+def test_image_file_type(file_type):
+    """
+    测试不同的file_type输入情况
+    """
+    mock = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
+
+    image = Image(mock, file_type=file_type)
+    assert image.format == file_type
+
+    data, buffer = image.parse()
+
+    # 返回文件名称
+    assert isinstance(data, str)
+    assert data.endswith(f".{file_type}")
+    assert buffer is not None
+    assert image.get_more() is None
+    assert image.get_config() is None
+
+
+def test_image_size():
+    """
+    测试不同的size输入情况
+    """
+    mock = np.random.randint(low=0, high=256, size=(256, 512, 3), dtype=np.uint8)
+    # 转为PIL图像后，size为(512, 256)
+
+    image = Image(mock, size=None)
+    assert image.image_size == (512, 256)
+
+    image = Image(mock, size=(128, 128))
+    assert image.image_size == (128, 128)
+
+    image = Image(mock, size=(128, None))
+    assert image.image_size == (128, 64)
+
+    image = Image(mock, size=(None, 128))
+    assert image.image_size == (256, 128)
+
+    image = Image(mock, size=128)
+    assert image.image_size == (128, 64)
diff --git a/test/unit/data/modules/line/pytest_line.py b/test/unit/data/modules/line/pytest_line.py
new file mode 100644
index 00000000..92689bbf
--- /dev/null
+++ b/test/unit/data/modules/line/pytest_line.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 15:43
+@File: pytest_line.py
+@IDE: pycharm
+@Description:
+    测试折线图模块
+"""
+import math
+import pytest
+from swanlab.error import DataTypeError
+from swanlab.data.modules import Line
+
+
+def test_line_ok():
+    """
+    正常的Line解析
+    """
+    line = Line(1)
+    data, raw = line.parse()
+    assert data == 1
+    assert raw is None
+    assert line.get_chart() == line.Chart.LINE
+    assert line.get_section() == "default"
+    assert line.get_more() is None
+    line = Line("1")
+    data, raw = line.parse()
+    assert data == 1
+    assert raw is None
+
+    class MockType:
+        def __float__(self):
+            return 1.
+
+    line = Line(MockType())
+    data, raw = line.parse()
+    assert data == 1
+    assert raw is None
+
+
+def test_line_nan():
+    """
+    NaN解析
+    """
+    line = Line("NaN")
+    data, raw = line.parse()
+    assert data == Line.nan
+    assert raw is None
+    line = Line(math.nan)
+    data, raw = line.parse()
+    assert data == Line.nan
+    assert raw is None
+
+
+def test_line_inf():
+    """
+    INF解析
+    """
+    line = Line("INF")
+    data, raw = line.parse()
+    assert data == Line.inf
+    assert raw is None
+    line = Line(math.inf)
+    data, raw = line.parse()
+    assert data == Line.inf
+    assert raw is None
+
+
+def test_line_error():
+    """
+    错误解析
+    """
+    line = Line("a")
+    with pytest.raises(DataTypeError) as e:
+        line.parse()
+    assert e.value.expected == "float"
+    assert e.value.got == "str"
+    line = Line([1])
+    with pytest.raises(DataTypeError) as e:
+        line.parse()
+    assert e.value.expected == "float"
+    assert e.value.got == "list"
+    line = Line(None)
+    with pytest.raises(DataTypeError) as e:
+        line.parse()
+    assert e.value.expected == "float"
+    assert e.value.got == "NoneType"
diff --git a/test/unit/data/modules/pytest_base.py b/test/unit/data/modules/pytest_base.py
new file mode 100644
index 00000000..30623505
--- /dev/null
+++ b/test/unit/data/modules/pytest_base.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/2 23:36
+@File: test_base.py
+@IDE: pycharm
+@Description:
+    测试基类
+"""
+from swanlab.data.modules.base import BaseType
+
+
+class MockType(BaseType):
+    def __init__(self, value):
+        super().__init__()
+        self.value = value
+
+    def parse(self):
+        return self.value, None
+
+    def get_chart(self):
+        return self.Chart.LINE
+
+    def get_section(self):
+        return "MockType"
+
+    def get_more(self):
+        return {"value": self.value}
+
+    def get_config(self):
+        return {"value": self.value}
+
+
+def test_base():
+    data = MockType(1)
+    assert data.parse() == (1, None)
+    assert data.get_chart() == data.Chart.LINE
+    assert data.get_section() == "MockType"
+    assert data.get_more() == {"value": 1}
+    assert data.get_config() == {"value": 1}
diff --git a/test/unit/data/modules/pytest_wrapper.py b/test/unit/data/modules/pytest_wrapper.py
new file mode 100644
index 00000000..20391122
--- /dev/null
+++ b/test/unit/data/modules/pytest_wrapper.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 15:42
+@File: pytest_wrapper.py
+@IDE: pycharm
+@Description:
+    测试包装器
+"""
+import swanlab.data.modules as M
+
+
+class TestWrapperLine:
+    def test_ok(self):
+        """
+        正常情况
+        """
+        data = M.Line(1)
+        wrapper = M.DataWrapper("test", [data])
+        assert wrapper.is_line is True
diff --git a/test/unit/data/modules/text/pytest_text.py b/test/unit/data/modules/text/pytest_text.py
new file mode 100644
index 00000000..55d31ca2
--- /dev/null
+++ b/test/unit/data/modules/text/pytest_text.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 15:44
+@File: pytest_text.py
+@IDE: pycharm
+@Description:
+    测试文本处理模块
+"""
+from swanlab.data.modules import Text
+import pytest
+
+
+def test_text_ok():
+    # ---------------------------------- 字符串输入 ----------------------------------
+    mock = "这是一段测试文本"
+    text = Text(data=mock)
+    data, buffer = text.parse()
+    assert data == mock
+    assert buffer is None
+    assert text.get_more() is None
+    assert text.get_config() is None
+    # ---------------------------------- float输入 ----------------------------------
+    mock = 1.0
+    text = Text(data=mock)
+    data, buffer = text.parse()
+    assert data == "1.0"
+    assert buffer is None
+    assert text.get_more() is None
+    assert text.get_config() is None
+    # ---------------------------------- int输入 ----------------------------------
+    mock = 1
+    text = Text(data=mock)
+    data, buffer = text.parse()
+    assert data == "1"
+    assert buffer is None
+    assert text.get_more() is None
+    assert text.get_config() is None
+
+
+def test_text_error_type():
+    mock = [1, 2, 3]
+    with pytest.raises(TypeError):
+        Text(data=mock)  # noqa
+
+
+def test_text_caption():
+    mock = "这是一段测试文本"
+    text = Text(data=mock, caption="test")
+    data, buffer = text.parse()
+    assert data == mock
+    assert buffer is None
+    assert text.get_more()["caption"] == "test"
+    assert text.get_config() is None
+    # ---------------------------------- float输入 ----------------------------------
+    mock = 1.0
+    text = Text(data=mock, caption="test")
+    data, buffer = text.parse()
+    assert data == "1.0"
+    assert buffer is None
+    assert text.get_more()["caption"] == "test"
+    assert text.get_config() is None
+    # ---------------------------------- int输入 ----------------------------------
+    mock = 1
+    text = Text(data=mock, caption="test")
+    data, buffer = text.parse()
+    assert data == "1"
+    assert buffer is None
+    assert text.get_more()["caption"] == "test"
+    assert text.get_config() is None
+    # ---------------------------------- int输入 ----------------------------------
+    mock = 1
+    text = Text(data=mock, caption="test")
+    data, buffer = text.parse()
+    assert data == "1"
+    assert buffer is None
+    assert text.get_more()["caption"] == "test"
+    assert text.get_config() is None
+    # ---------------------------------- int输入 ----------------------------------
+    mock = 1
+    text = Text(data=mock, caption="test")
+    data, buffer = text.parse()
+    assert data == "1"
+    assert buffer is None
+    assert text.get_more()["caption"] == "test"
+    assert text.get_config() is None
+    # ---------------------------------- int输入 ----------------------------------
+    mock = 1
+    text = Text(data=mock, caption="test")
+    data, buffer = text.parse()
+    assert data == "1"
+    assert buffer is None
+    assert text.get_more()["caption"] == "test"
+    assert text.get_config() is None
+    # ---------------------------------- int输入 ----------------------------------
+    mock = 1
+    text = Text(data=mock, caption="test")
+    data, buffer = text.parse()
+    assert data == "1"
+    assert buffer is None
+    assert text.get_more()["caption"] == "test"
+    assert text.get_config() is None
diff --git a/test/unit/data/pytest_sdk.py b/test/unit/data/pytest_sdk.py
index 259722f5..9dbee356 100644
--- a/test/unit/data/pytest_sdk.py
+++ b/test/unit/data/pytest_sdk.py
@@ -8,7 +8,6 @@
     测试sdk的一些api
 """
 from swanlab.env import (
-    reset_env,
     get_swanlab_folder,
     MODE,
     ROOT,
@@ -67,7 +66,7 @@ def test_init_cloud(self):
 
     def test_init_error(self):
         with pytest.raises(ValueError):
-            S.init(mode="123456")
+            S.init(mode="123456")  # noqa
         assert get_run() is None
 
     # ---------------------------------- 测试环境变量输入 ----------------------------------
diff --git a/test/unit/data/run/pytest_main.py b/test/unit/data/run/pytest_main.py
index c8561d89..a1dfc172 100644
--- a/test/unit/data/run/pytest_main.py
+++ b/test/unit/data/run/pytest_main.py
@@ -7,19 +7,18 @@
 @Description:
     测试SwanLabRun主类
 """
-import math
-
+from swanlab.data.modules import Line
 from swanlab.data.run.main import SwanLabRun, get_run, SwanLabRunState, swanlog
 from swanlab import Image, Audio, Text
 from nanoid import generate
 from tutils import clear, TEMP_PATH
 from PIL import Image as PILImage
-import torch
 import soundfile as sf
 import numpy as np
+import math
 import pytest
-import random
 import os
+import io
 
 
 @pytest.fixture(scope="function", autouse=True)
@@ -63,555 +62,214 @@ def test_duplicate_init(self):
         assert swanlog.installed is False
 
 
-class TestSwanLabRunLog:
+class TestSwanLabRunState:
     """
-    测试SwanLabRun的日志解析功能
-    1. 输入为字典
-    2. 可包含参数step
-    3. 输入的字典的key必须为字符串
-    4. 输出解析后的数据对象，包含额外的信息，step等信息
-    5. 如果某一个解析失败，对应的key存在，但返回的数据为None
+    测试SwanLabRun的状态变化
     """
 
-    @pytest.mark.parametrize(
-        "data",
-        [
-            random.randint(1, 100),
-            -random.randint(1, 100),
-            random.random(),
-        ],
-    )
-    def test_log_number_ok(self, data):
-        """
-        测试解析一个正常的数字
-        """
-        run = SwanLabRun()
-        metric_dict = run.log({"a": data})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert len(a.metric) == 3
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        assert a.metric["data"] == data
-        assert a.step == 0
-        assert a.epoch == 1
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "default"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "default"
-        assert ac.namespace == "default"
-        assert ac.reference == "step"
+    def test_not_started(self):
+        assert SwanLabRun.get_state() == SwanLabRunState.NOT_STARTED
 
-    def test_log_number_nan(self):
-        """
-        测试解析一个nan
-        """
+    def test_running(self):
         run = SwanLabRun()
-        metric_dict = run.log({"a": float("nan")})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert a.metric is None
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "default"
-        assert ac.error["data_class"] == "NaN"
-        assert ac.error["excepted"] == ["float", "int"]
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "default"
-        assert ac.namespace == "default"
-        assert ac.reference == "step"
+        assert run.state == SwanLabRunState.RUNNING
+        assert run.is_running is True
 
-    def test_log_number_inf(self):
-        """
-        测试解析一个inf
-        """
+    def test_crashed(self):
         run = SwanLabRun()
-        metric_dict = run.log({"float(inf)": float("inf"), "math.inf": math.inf})
-        assert metric_dict["float(inf)"] is not None and metric_dict["math.inf"] is not None
-        assert len(metric_dict) == 2
-        a1 = metric_dict["float(inf)"]
-        a2 = metric_dict["math.inf"]
-        ac1 = a1.column_info
-        ac2 = a2.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert a1.metric is None
-        assert a2.metric is None
-        # ---------------------------------- float(inf)信息 ----------------------------------
-        assert ac1.data_type == "default"
-        assert ac1.error["data_class"] == "INF"
-        assert ac1.error["excepted"] == ["float", "int"]
-        # 默认排在最前面
-        assert ac1.sort == 0
-        assert ac1.config == {}
-        assert ac1.key == "float(inf)"
-        assert ac1.chart_type == "default"
-        assert ac1.namespace == "default"
-        assert ac1.reference == "step"
-        # ---------------------------------- math.inf信息 ----------------------------------
-        assert ac2.data_type == "default"
-        assert ac2.error["data_class"] == "INF"
-        assert ac2.error["excepted"] == ["float", "int"]
-        # 默认排在最前面
-        assert ac2.sort == 0
-        assert ac2.config == {}
-        assert ac2.key == "math.inf"
-        assert ac2.chart_type == "default"
-        assert ac2.namespace == "default"
-        assert ac2.reference == "step"
+        run.finish(SwanLabRunState.CRASHED, error="error")
+        assert run.state == SwanLabRunState.CRASHED
+        assert run.is_crashed is True
 
-    def test_log_number_str(self):
-        """
-        测试解析其他字符串
-        """
-        """
-        测试解析一个nan
-        """
+    def test_success(self):
         run = SwanLabRun()
-        metric_dict = run.log({"a": "abc"})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert a.metric is None
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "default"
-        assert ac.error["data_class"] == "str"
-        assert ac.error["excepted"] == ["float", "int"]
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "default"
-        assert ac.namespace == "default"
-        assert ac.reference == "step"
+        run.finish(SwanLabRunState.SUCCESS)
+        assert run.state == SwanLabRunState.SUCCESS
+        assert run.is_success is True
 
-    def test_log_number_use_step(self):
-        """
-        测试解析一个数字，使用step
-        """
-        run = SwanLabRun()
-        metric_dict = run.log({"a": 1}, step=1)
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert len(a.metric) == 3
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        assert a.metric["data"] == 1
-        assert a.step == 1
-        assert a.epoch == 1
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "default"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "default"
-        assert ac.namespace == "default"
-        assert ac.reference == "step"
 
-    def test_log_number_use_step_duplicate(self):
-        """
-        测试解析一个数字，使用step，但是重复执行
-        """
-        run = SwanLabRun()
-        run.log({"a": 1}, step=1)
-        metric_dict = run.log({"a": 1}, step=1)
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        assert a.error is True
+class TestSwanLabRunLog:
+    """
+    测试SwanLabRun的日志解析功能，不包含操作员，由于情况比较多，这里分情况测试
+    1. 一开始正常输入，然后重复输入，然后输入新的key的重复的step
+    2. 一开始输入错误的类型，然后正常输入
+    3. 一开始输入正确的类型，然后输入错误的类型
+    """
 
-    def test_log_number_use_prefix(self):
-        """
-        测试解析一个数字，使用prefix
-        """
+    # ---------------------------------- 解析log数字/Line ----------------------------------
+    def test_log_number_ok(self):
         run = SwanLabRun()
-        prefix_1 = generate()
-        prefix_2 = generate() + "/" + generate()
-        key1 = f"{prefix_1}/a"
-        key2 = f"{prefix_2}/a"
-        metric_dict = run.log({key1: 1, key2: 1})
-        assert len(metric_dict) == 2
-        for key in metric_dict:
-            assert metric_dict[key] is not None
-            a = metric_dict[key]
-            ac = a.column_info
-            # ---------------------------------- 指标信息 ----------------------------------
-            assert len(a.metric) == 3
-            assert "index" in a.metric
-            assert "create_time" in a.metric
-            assert "data" in a.metric
-            assert a.metric["data"] == 1
-            assert a.step == 0
-            assert a.epoch == 1
-            # ---------------------------------- 列信息 ----------------------------------
-            assert ac.data_type == "default"
-            assert ac.error is None
-            # 默认排在最前面
-            assert ac.sort is None
-            assert ac.config == {}
-            assert ac.key == key
-            assert ac.chart_type == "default"
-            assert ac.namespace == key.split("/")[0]
-            assert ac.reference == "step"
-
-    def test_log_image_path(self):
-        """
-        测试解析一个图片, 使用文件路径
+        data = {"a": 1, "b": 0.1, "math.nan": math.nan, "math.inf": math.inf}
+        ll = run.log(data)
+        assert len(ll) == 4
+        # 都没有错误
+        assert all([ll[k].error is False for k in ll])
+        assert ll["a"].data == 1
+        assert ll["b"].data == 0.1
+        assert ll["math.nan"].data == Line.nan
+        assert ll["math.inf"].data == Line.inf
+        assert all([ll[k].column_info.chart == ll[k].column_info.chart.LINE for k in ll])
+        # 没有其他多余的内容
+        assert all([ll[k].buffers is None for k in ll])
+        assert all([ll[k].data is not None for k in ll])
+        # 没有指定step的话从0开始
+        assert all([ll[k].step == 0 for k in ll])
+        ll2 = run.log(data, step=3)
+        assert all(ll2[k].step == 3 for k in ll2)
+        # 重复的step会被忽略
+        ll3 = run.log(data, step=3)
+        assert all(ll3[k].error for k in ll3)
+        assert all(ll3[k].duplicated_error for k in ll3)
+        assert all(ll3[k].column_error_info is None for k in ll3)
+        assert all(ll3[k].error_info is not None for k in ll3)
+        # 如果是新的key的重复step，会被添加
+        ll4 = run.log({"tmp": 1}, step=3)
+        assert all(ll4[k].error is False for k in ll4)
+        assert all(ll4[k].step == 3 for k in ll4)
+
+    def test_log_number_use_line(self):
+        """
+        使用Line类型log，本质上应该与数字类型一样，数字类型是Line类型的语法糖
         """
-        # 创建随机图像，并保存到TEMP目录
-        random_im = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
-        random_im_pil = PILImage.fromarray(random_im)
-        save_path = os.path.join(TEMP_PATH, "a.jpg")
-        random_im_pil.save(save_path)
-
         run = SwanLabRun()
-        metric_dict = run.log({"a": Image(save_path)})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "image"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "image"
-        assert ac.namespace == "Image"
-        assert ac.reference == "step"
-
-    def test_log_image_np(self):
-        """
-        测试解析一个图片，使用np.ndarray
+        data = {"a": Line(1), "b": Line(0.1), "math.nan": Line(math.nan), "math.inf": Line(math.inf)}
+        ll = run.log(data)
+        assert len(ll) == 4
+        # line(1)和[line(1)]是一样的
+        ll2 = run.log({"a": [Line(1)]})
+        assert ll2["a"].data == ll["a"].data
+        # Line类型不允许多个元素
+        ll3 = run.log({"a": [Line(1), Line(2)]})
+        assert ll3["a"].error is True
+        assert ll3["a"].column_error_info is None
+        assert ll3["a"].error_info.expected == 'float'
+
+    def test_log_number_error_type(self):
+        """
+        使用错误的，不受支持的类型log
         """
-
-        random_im = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
-
         run = SwanLabRun()
-        metric_dict = run.log({"a": Image(random_im)})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "image"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "image"
-        assert ac.namespace == "Image"
-        assert ac.reference == "step"
+        data = {"a": "a", "b": [1], "c": 1}
+        ll = run.log(data)
+        assert len(ll) == 3
+        # 前两个错误，最后一个正确
+        assert ll["a"].error is True
+        assert ll["b"].error is True
+        assert ll["c"].error is False
 
-    def test_log_image_pil(self):
-        """
-        测试解析一个图片，使用PIL.Image
-        """
+        assert ll["a"].column_error_info is not None
+        assert ll["b"].column_error_info is not None
+        assert ll["c"].column_error_info is None
 
-        random_im = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
-        random_im_pil = PILImage.fromarray(random_im)
+        assert ll["a"].column_error_info.expected == 'float'
+        assert ll["b"].column_error_info.expected == 'float'
+        assert ll["c"].data == 1
 
-        run = SwanLabRun()
-        metric_dict = run.log({"a": Image(random_im_pil)})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "image"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "image"
-        assert ac.namespace == "Image"
-        assert ac.reference == "step"
+        assert ll["a"].error_info is None
+        assert ll["b"].error_info is None
 
-    def test_log_image_plt(self):
-        """
-        测试解析一个图片，使用Matplotlib
-        """
-        import matplotlib.pyplot as plt
-
-        x = [1, 2, 3, 4, 5]
-        y = [2, 3, 5, 7, 11]
-        plt.plot(x, y)
-        plt.title("Examples")
-        plt.xlabel("X-axis")
-        plt.ylabel("Y-axis")
+    # ---------------------------------- 解析log文字 ----------------------------------
 
+    def test_log_text_ok(self):
         run = SwanLabRun()
-        metric_dict = run.log({"a": Image(plt)})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "image"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "image"
-        assert ac.namespace == "Image"
-        assert ac.reference == "step"
-
-    def test_log_image_tensor(self):
-        """
-        测试解析一个图片，使用pytorch tensor
-        """
-        random_im = torch.randn(4, 3, 64, 64)
-
-        run = SwanLabRun()
-        metric_dict = run.log({"a": Image(random_im)})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "image"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "image"
-        assert ac.namespace == "Image"
-        assert ac.reference == "step"
-
-    def test_log_image_batch(self):
-        """
-        测试解析一次log一批图片
-        """
-        random_im = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8)
-
-        run = SwanLabRun()
-        metric_dict = run.log({"a": [Image(random_im)] * 3})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "image"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "image"
-        assert ac.namespace == "Image"
-        assert ac.reference == "step"
+        data = {"a": Text("abc"), "b": Text("def")}
+        ll = run.log(data)
+        assert len(ll) == 2
+        assert all([ll[k].error is False for k in ll])
+        assert ll["a"].data == ["abc"]
+        assert ll["b"].data == ["def"]
+        assert all([ll[k].column_info.chart == ll[k].column_info.chart.TEXT for k in ll])
+        assert all([ll[k].buffers is None for k in ll])
+        assert all([ll[k].data is not None for k in ll])
+        assert all([ll[k].step == 0 for k in ll])
+        ll2 = run.log(data, step=3)
+        assert all(ll2[k].step == 3 for k in ll2)
+        # list
+        ll3 = run.log({"a": [Text("abc"), Text("def")]})
+        assert ll3["a"].data == ["abc", "def"]
+
+    # ---------------------------------- 解析log Audio ----------------------------------
 
     def test_log_audio_path(self):
         """
-        测试解析一个音频，使用文件路径
+        通过路径添加音频
         """
-        sample_rate = 44100
-        t = np.linspace(0, 1, sample_rate, False)
-        frequency = 440
-        audio_signal = 0.5 * np.sin(2 * np.pi * frequency * t)
-        save_path = os.path.join(TEMP_PATH, "output.wav")
-        sf.write(save_path, audio_signal, sample_rate)
-
         run = SwanLabRun()
-        metric_dict = run.log({"a": Audio(save_path)})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "audio"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "audio"
-        assert ac.namespace == "Audio"
-        assert ac.reference == "step"
-
-    def test_log_audio_np(self):
-        """
-        测试解析一个音频，使用np.ndarray
+        # 正确的音频路径
+        path = os.path.join(TEMP_PATH, "test.wav")
+        samplerate = 5000
+        sf.write(path, np.random.rand(1000), samplerate)
+        data = {"a": Audio(path)}
+        ll = run.log(data)
+        assert len(ll["a"].buffers) == 1
+        buffer = ll["a"].buffers[0]
+        # 通过字节流解码后的采样点数
+        audio, _samplerate = sf.read(io.BytesIO(buffer.getvalue()))
+        assert _samplerate == samplerate
+
+    def test_log_audio_numpy(self):
+        """
+        通过numpy数组添加音频
         """
-        random_audio = np.random.randn(2, 100000)
-
         run = SwanLabRun()
-        metric_dict = run.log({"a": Audio(random_audio)})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "audio"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "audio"
-        assert ac.namespace == "Audio"
-        assert ac.reference == "step"
+        samplerate = 5000
+        data = {"a": Audio(np.random.rand(1000), samplerate)}
+        ll = run.log(data)
+        assert len(ll["a"].buffers) == 1
+        buffer = ll["a"].buffers[0]
+        # 通过字节流解码后的采样点数
+        audio, _samplerate = sf.read(io.BytesIO(buffer.getvalue()))
+        assert _samplerate == samplerate
+        # FIXME 一维的？
+        assert len(audio.shape) == 1
 
-    def test_log_audio_batch(self):
+    def test_log_audio_error(self):
         """
-        测试解析一批音频
+        错误的情况
         """
-        random_audio = np.random.randn(2, 100000)
-
         run = SwanLabRun()
-        metric_dict = run.log({"a": [Audio(random_audio)] * 3})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "audio"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "audio"
-        assert ac.namespace == "Audio"
-        assert ac.reference == "step"
+        # 错误的路径
+        with pytest.raises(ValueError):
+            run.log({"a": Audio("error.wav")})
+        # 错误的类型
+        with pytest.raises(TypeError):
+            run.log({"a": Audio(1)})  # noqa
+        # 错误的矩阵编码类型
+        with pytest.raises(TypeError):
+            run.log({"a": Audio(np.random.rand(1000).astype(np.int8))})
+        # 错误的矩阵维数
+        with pytest.raises(TypeError):
+            run.log({"a": Audio(np.random.rand(1000, 5))})
+
+    # ---------------------------------- 图像 ----------------------------------
 
-    def test_log_text_str(self):
+    def test_log_image_path(self):
         """
-        测试解析单个字符串文本
+        通过路径添加图像
         """
-        random_text = "Hello World"
-
         run = SwanLabRun()
-        metric_dict = run.log({"a": Text(random_text)})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "text"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "text"
-        assert ac.namespace == "Text"
-        assert ac.reference == "step"
+        # 正确的图像路径
+        path = os.path.join(TEMP_PATH, "test.png")
+        PILImage.new("RGB", (100, 100)).save(path)
+        data = {"a": Image(path)}
+        ll = run.log(data)
+        assert len(ll["a"].buffers) == 1
+        buffer = ll["a"].buffers[0]
+        # 通过字节流解码后的图像
+        image = PILImage.open(io.BytesIO(buffer.getvalue()))
+        assert image.size == (100, 100)
 
-    def test_log_text_bacth(self):
+    def test_log_image_numpy(self):
         """
-        测试解析一批文本
+        通过numpy数组添加图像
         """
-        random_text = "Hello World"
-
         run = SwanLabRun()
-        metric_dict = run.log({"a": [Text(random_text)] * 3})
-        assert metric_dict["a"] is not None
-        assert len(metric_dict) == 1
-        a = metric_dict["a"]
-        ac = a.column_info
-        # ---------------------------------- 指标信息 ----------------------------------
-        assert "index" in a.metric
-        assert "create_time" in a.metric
-        assert "data" in a.metric
-        # ---------------------------------- 列信息 ----------------------------------
-        assert ac.data_type == "text"
-        assert ac.error is None
-        # 默认排在最前面
-        assert ac.sort == 0
-        assert ac.config == {}
-        assert ac.key == "a"
-        assert ac.chart_type == "text"
-        assert ac.namespace == "Text"
-        assert ac.reference == "step"
-
-
-class TestSwanLabRunState:
-    """
-    测试SwanLabRun的状态变化
-    """
-
-    def test_not_started(self):
-        assert SwanLabRun.get_state() == SwanLabRunState.NOT_STARTED
-
-    def test_running(self):
-        run = SwanLabRun()
-        assert run.state == SwanLabRunState.RUNNING
-        assert run.is_running is True
-
-    def test_crashed(self):
-        run = SwanLabRun()
-        run.finish(SwanLabRunState.CRASHED, error="error")
-        assert run.state == SwanLabRunState.CRASHED
-        assert run.is_crashed is True
-
-    def test_success(self):
-        run = SwanLabRun()
-        run.finish(SwanLabRunState.SUCCESS)
-        assert run.state == SwanLabRunState.SUCCESS
-        assert run.is_success is True
+        data = {"a": Image(np.random.rand(100, 100, 3))}
+        ll = run.log(data)
+        assert len(ll["a"].buffers) == 1
+        buffer = ll["a"].buffers[0]
+        # 通过字节流解码后的图像
+        image = PILImage.open(io.BytesIO(buffer.getvalue()))
+        assert image.size == (100, 100)
+
+    # 其他类似...
diff --git a/test/unit/data/run/test_exp.py b/test/unit/data/run/test_exp.py
new file mode 100644
index 00000000..af02797a
--- /dev/null
+++ b/test/unit/data/run/test_exp.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/3 01:26
+@File: test_exp.py
+@IDE: pycharm
+@Description:
+    测试运行实验类
+"""
+
+
+def test_exp_add_ok():
+    pass
+
+
+def test_exp_add_wrong_type():
+    pass
diff --git a/test/unit/utils/pytest_file.py b/test/unit/utils/pytest_file.py
index 3b6c4746..00e39af6 100644
--- a/test/unit/utils/pytest_file.py
+++ b/test/unit/utils/pytest_file.py
@@ -9,33 +9,40 @@
 """
 import pytest
 from nanoid import generate
+
 # noinspection PyProtectedMember
 from swanlab.utils.file import (
     check_proj_name_format,
     check_exp_name_format,
     _auto_cut,
     check_desc_format,
-    check_tag_format,
+    check_key_format,
 )
 
 
 class TestAutoCut:
-    @pytest.mark.parametrize("name, value", [
-        [generate(), generate(size=101)],
-        [generate(), generate(size=1000)],
-        [generate(), generate(size=10000)],
-    ])
+    @pytest.mark.parametrize(
+        "name, value",
+        [
+            [generate(), generate(size=101)],
+            [generate(), generate(size=1000)],
+            [generate(), generate(size=10000)],
+        ],
+    )
     def test_cut(self, name: str, value: str):
         """
         测试自动截断
         """
         assert len(_auto_cut(name, value, 100, True)) == 100
 
-    @pytest.mark.parametrize("name, value", [
-        [generate(), generate(size=101)],
-        [generate(), generate(size=1000)],
-        [generate(), generate(size=10000)],
-    ])
+    @pytest.mark.parametrize(
+        "name, value",
+        [
+            [generate(), generate(size=101)],
+            [generate(), generate(size=1000)],
+            [generate(), generate(size=10000)],
+        ],
+    )
     def test_no_cut(self, name: str, value: str):
         """
         测试不自动截断
@@ -46,24 +53,22 @@ def test_no_cut(self, name: str, value: str):
 
 
 class TestProjName:
-    @pytest.mark.parametrize("value", [
-        generate(size=100),
-        generate(size=1),
-        "-",
-        "_",
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            generate(size=100),
+            generate(size=1),
+            "-",
+            "_",
+        ],
+    )
     def test_proj_name_common(self, value):
         """
         测试正常情况
         """
         assert check_proj_name_format(value) == value
 
-    @pytest.mark.parametrize("value", [
-        None,
-        1,
-        [],
-        {}
-    ])
+    @pytest.mark.parametrize("value", [None, 1, [], {}])
     def test_proj_name_type_error(self, value: str):
         """
         测试类型错误
@@ -71,13 +76,7 @@ def test_proj_name_type_error(self, value: str):
         with pytest.raises(TypeError):
             check_proj_name_format(value)
 
-    @pytest.mark.parametrize("value", [
-        "",
-        " "
-        "啊哈哈",
-        "&^%",
-        "/;]x]"
-    ])
+    @pytest.mark.parametrize("value", ["", " " "啊哈哈", "&^%", "/;]x]"])
     def test_proj_name_value_error(self, value: str):
         """
         测试空值或者不合法值
@@ -85,22 +84,28 @@ def test_proj_name_value_error(self, value: str):
         with pytest.raises(ValueError):
             check_proj_name_format(value)
 
-    @pytest.mark.parametrize("value", [
-        generate(size=101),
-        generate(size=1000),
-        generate(size=10000),
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            generate(size=101),
+            generate(size=1000),
+            generate(size=10000),
+        ],
+    )
     def test_proj_name_auto_cut(self, value: str):
         """
         测试自动截断
         """
         assert len(check_proj_name_format(value)) == 100
 
-    @pytest.mark.parametrize("value", [
-        generate(size=101),
-        generate(size=1000),
-        generate(size=10000),
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            generate(size=101),
+            generate(size=1000),
+            generate(size=10000),
+        ],
+    )
     def test_proj_name_no_cut(self, value: str):
         """
         测试不自动截断
@@ -110,24 +115,22 @@ def test_proj_name_no_cut(self, value: str):
 
 
 class TestExpName:
-    @pytest.mark.parametrize("value", [
-        generate(size=100),
-        generate(size=1),
-        "-",
-        "_",
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            generate(size=100),
+            generate(size=1),
+            "-",
+            "_",
+        ],
+    )
     def test_exp_name_common(self, value):
         """
         测试正常情况
         """
         assert check_exp_name_format(value) == value
 
-    @pytest.mark.parametrize("value", [
-        None,
-        1,
-        [],
-        {}
-    ])
+    @pytest.mark.parametrize("value", [None, 1, [], {}])
     def test_exp_name_type_error(self, value: str):
         """
         测试类型错误
@@ -135,23 +138,20 @@ def test_exp_name_type_error(self, value: str):
         with pytest.raises(TypeError):
             check_exp_name_format(value)
 
-    @pytest.mark.parametrize("value", [
-        "啊哈哈",
-        "&^%",
-        "/;]x]",
-        generate(size=100),
-        "👨‍💻👨‍💻👨‍💻👨‍💻👨‍"
-    ])
+    @pytest.mark.parametrize("value", ["啊哈哈", "&^%", "/;]x]", generate(size=100), "👨‍💻👨‍💻👨‍💻👨‍💻👨‍"])
     def test_exp_name_value_special(self, value: str):
         """
         测试特殊值
         """
         assert check_exp_name_format(value) == value
 
-    @pytest.mark.parametrize("value", [
-        "",
-        "   ",
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            "",
+            "   ",
+        ],
+    )
     def test_exp_name_value_empty(self, value: str):
         """
         测试空值
@@ -159,22 +159,28 @@ def test_exp_name_value_empty(self, value: str):
         with pytest.raises(ValueError):
             check_exp_name_format(value)
 
-    @pytest.mark.parametrize("value", [
-        generate(size=101),
-        generate(size=1000),
-        generate(size=10000),
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            generate(size=101),
+            generate(size=1000),
+            generate(size=10000),
+        ],
+    )
     def test_exp_name_auto_cut(self, value: str):
         """
         测试自动截断
         """
         assert len(check_exp_name_format(value)) == 100
 
-    @pytest.mark.parametrize("value", [
-        generate(size=101),
-        generate(size=1000),
-        generate(size=10000),
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            generate(size=101),
+            generate(size=1000),
+            generate(size=10000),
+        ],
+    )
     def test_exp_name_no_cut(self, value: str):
         """
         测试不自动截断
@@ -184,25 +190,14 @@ def test_exp_name_no_cut(self, value: str):
 
 
 class TestDesc:
-    @pytest.mark.parametrize("value", [
-        generate(size=100),
-        generate(size=1),
-        "-",
-        "_",
-        "👾👾👾👾👾👾"
-    ])
+    @pytest.mark.parametrize("value", [generate(size=100), generate(size=1), "-", "_", "👾👾👾👾👾👾"])
     def test_desc_common(self, value):
         """
         测试正常情况
         """
         assert check_desc_format(value) == value
 
-    @pytest.mark.parametrize("value", [
-        None,
-        1,
-        [],
-        {}
-    ])
+    @pytest.mark.parametrize("value", [None, 1, [], {}])
     def test_desc_type_error(self, value: str):
         """
         测试类型错误
@@ -210,11 +205,7 @@ def test_desc_type_error(self, value: str):
         with pytest.raises(TypeError):
             check_desc_format(value)
 
-    @pytest.mark.parametrize("value", [
-        "",
-        "   ",
-        " " * 256
-    ])
+    @pytest.mark.parametrize("value", ["", "   ", " " * 256])
     def test_desc_value_empty(self, value: str):
         """
         测试空值
@@ -223,65 +214,73 @@ def test_desc_value_empty(self, value: str):
 
 
 class TestTag:
-    @pytest.mark.parametrize("value", [
-        generate(size=255),
-        generate(size=100),
-        generate(size=1),
-        "12/",
-        "-",
-        "_",
-        "👾👾👾👾👾👾"
-    ])
+    @pytest.mark.parametrize(
+        "value", [generate(size=255), generate(size=100), generate(size=1), "12/", "-", "_", "👾👾👾👾👾👾"]
+    )
     def test_tag_common(self, value):
         """
         测试正常情况
         """
-        assert check_tag_format(value) == value
+        assert check_key_format(value) == value
+
+    def test_key_blank(self):
+        """
+        测试收尾空格情况
+        """
+        with pytest.raises(ValueError):
+            check_key_format("  ")
+        key = "  " + "abc" + "  "
+        assert check_key_format(key) == "abc"
 
-    @pytest.mark.parametrize("value", [
-        None,
-        1,
-        [],
-        {}
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            None,
+            1,
+            [],
+            {},
+        ],
+    )
     def test_tag_type_error(self, value: str):
         """
         测试类型错误
         """
         with pytest.raises(TypeError):
-            check_tag_format(value)
+            check_key_format(value)
 
-    @pytest.mark.parametrize("value", [
-        "",
-        "   ",
-        " " * 256
-    ])
-    def test_tag_value_empty(self, value: str):
+    @pytest.mark.parametrize("value", ["", "   ", " " * 256, ".sas", "/asa"])
+    def test_tag_value_error(self, value: str):
         """
         测试空值
         """
         with pytest.raises(ValueError):
-            check_tag_format(value)
+            check_key_format(value)
 
-    @pytest.mark.parametrize("value", [
-        generate(size=256),
-        generate(size=1000),
-        generate(size=10000),
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            generate(size=256),
+            generate(size=1000),
+            generate(size=10000),
+        ],
+    )
     def test_tag_auto_cut(self, value: str):
         """
         测试自动截断
         """
-        assert len(check_tag_format(value)) == 255
+        assert len(check_key_format(value)) == 255
 
-    @pytest.mark.parametrize("value", [
-        generate(size=256),
-        generate(size=1000),
-        generate(size=10000),
-    ])
+    @pytest.mark.parametrize(
+        "value",
+        [
+            generate(size=256),
+            generate(size=1000),
+            generate(size=10000),
+        ],
+    )
     def test_tag_no_cut(self, value: str):
         """
         测试不自动截断
         """
         with pytest.raises(IndexError):
-            check_tag_format(value, auto_cut=False)
+            check_key_format(value, auto_cut=False)