labelstudio-集成规范.md 15 KB

使用中文与用户对话

LabelStudio 编辑器集成规范

概述

本文档记录了在 React 应用中集成 LabelStudio 编辑器的完整流程、常见问题和解决方案。这些经验来自实际项目中遇到的样式问题和配置挑战。

核心原则

  1. 动态导入编辑器:使用 import('@humansignal/editor') 动态加载,不要静态导入
  2. CSS 模块前缀:必须配置 webpack 添加 lsf- 前缀
  3. 样式加载顺序:只导入 UI 库样式,让编辑器样式自动加载
  4. 参考成功案例:遵循 playground 和 labelstudio 应用的实现方式

项目结构

web/
├── apps/
│   ├── lq_label/              # 你的应用
│   ├── playground/            # 参考:简单的编辑器集成
│   └── labelstudio/           # 参考:完整的 Label Studio 应用
├── libs/
│   ├── editor/                # LabelStudio 编辑器库
│   └── ui/                    # UI 组件库
└── webpack.config.js          # 根配置(包含 CSS 前缀处理)

集成步骤

1. Webpack 配置(最关键)

在应用的 webpack.config.js 中添加 CSS 模块配置:

const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const webpack = require('webpack');

const css_prefix = 'lsf-';

module.exports = composePlugins(
  withNx({ skipTypeChecking: true }),
  withReact(),
  (config) => {
    // 1. 定义 CSS_PREFIX 环境变量
    config.plugins = [
      ...config.plugins,
      new webpack.DefinePlugin({
        'process.env.CSS_PREFIX': JSON.stringify(css_prefix),
      }),
    ];

    // 2. 配置 CSS 模块添加 lsf- 前缀
    config.module.rules.forEach((rule) => {
      const testString = rule.test?.toString() || '';
      
      if (rule.test?.toString().match(/scss|sass/) && !testString.includes('.module')) {
        const r = rule.oneOf?.filter((r) => {
          if (!r.use) return false;
          const testString = r.test?.toString() || '';
          if (testString.match(/module|raw|antd/)) return false;
          return testString.match(/scss|sass/) && 
                 r.use.some((u) => u.loader && u.loader.includes('css-loader'));
        });

        r?.forEach((_r) => {
          const cssLoader = _r.use.find((use) => 
            use.loader && use.loader.includes('css-loader')
          );
          
          if (!cssLoader) return;

          const isSASS = _r.use.some((use) => 
            use.loader && use.loader.match(/sass|scss/)
          );
          
          if (isSASS) _r.exclude = /node_modules/;

          if (cssLoader.options) {
            cssLoader.options.modules = {
              localIdentName: `${css_prefix}[local]`,
              getLocalIdent(_ctx, _ident, className) {
                if (className.includes('ant')) return className;
              },
            };
          }
        });
      }
    });

    // 3. 其他必要配置
    config.experiments = {
      ...config.experiments,
      asyncWebAssembly: true,
      syncWebAssembly: true,
      topLevelAwait: true,
    };

    config.resolve = {
      ...config.resolve,
      fallback: {
        path: false,
        fs: false,
        crypto: false,
        stream: false,
        buffer: false,
        util: false,
        process: false,
      },
    };

    return config;
  }
);

关键点

  • css_prefix = 'lsf-':所有 LabelStudio 的 CSS 类名前缀
  • localIdentName: 'lsf-[local]':将 .label 转换为 .lsf-label
  • 排除 .module.scss 文件(它们有自己的命名规则)
  • 保留 ant 开头的类名(Ant Design 组件)

2. 样式导入(main.tsx)

正确的方式

// main.tsx
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './app/app';

// 只导入 UI 库样式
// LabelStudio 编辑器样式会在动态导入时自动加载
import '../../../libs/ui/src/styles.scss';
import '../../../libs/ui/src/tailwind.css';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>
);

错误的方式(不要这样做):

// ❌ 不要静态导入 editor 的全局样式
import '../../../libs/editor/src/assets/styles/global.scss';

3. 组件实现

参考 playground 的 PreviewPanel 组件实现:

import React, { useEffect, useState, useRef } from 'react';
import { onSnapshot } from 'mobx-state-tree';

// 清除 localStorage
if (typeof localStorage !== 'undefined') {
  localStorage.removeItem('labelStudio:settings');
}

export const EditorComponent: React.FC = () => {
  const [editorReady, setEditorReady] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  const editorContainerRef = useRef<HTMLDivElement>(null);
  const lsfInstanceRef = useRef<any>(null);
  const rafIdRef = useRef<number | null>(null);

  useEffect(() => {
    let LabelStudio: any;
    let dependencies: any;
    let snapshotDisposer: any;

    function cleanup() {
      if (typeof window !== 'undefined' && (window as any).LabelStudio) {
        delete (window as any).LabelStudio;
      }
      setEditorReady(false);
      if (lsfInstanceRef.current) {
        try {
          lsfInstanceRef.current.destroy();
        } catch {
          // Ignore cleanup errors in HMR scenarios
        }
        lsfInstanceRef.current = null;
      }
      if (rafIdRef.current !== null) {
        cancelAnimationFrame(rafIdRef.current);
        rafIdRef.current = null;
      }
      if (snapshotDisposer) {
        snapshotDisposer();
        snapshotDisposer = null;
      }
    }

    async function loadLSF() {
      try {
        // 动态导入 LabelStudio
        dependencies = await import('@humansignal/editor');
        LabelStudio = dependencies.LabelStudio;
        
        if (!LabelStudio) {
          setError('编辑器加载失败');
          return;
        }

        cleanup();
        setEditorReady(true);

        // 初始化 LabelStudio 实例
        setTimeout(() => {
          if (!editorContainerRef.current) return;

          lsfInstanceRef.current = new LabelStudio(editorContainerRef.current, {
            config: yourConfig,
            task: yourTask,
            interfaces: [
              'panel',
              'update',
              'submit',
              'controls',
              'side-column',
              'annotations:menu',
              'annotations:add-new',
              'annotations:delete',
              'predictions:menu',
            ],
            instanceOptions: {
              reactVersion: 'v18',
            },
            settings: {
              forceBottomPanel: true,
              collapsibleBottomPanel: true,
              defaultCollapsedBottomPanel: false,
              fullscreen: false,
            },
            onStorageInitialized: (LS: any) => {
              const initAnnotation = () => {
                const as = LS.annotationStore;
                const annotation = as.createAnnotation();
                as.selectAnnotation(annotation.id);

                if (annotation) {
                  snapshotDisposer = onSnapshot(annotation, () => {
                    // 处理标注更新
                    const result = annotation.serializeAnnotation();
                    console.log('Annotation updated:', result);
                  });
                }
              };
              setTimeout(initAnnotation);
            },
          });
        });
      } catch (err: any) {
        console.error('Error loading LabelStudio:', err);
        setError(err.message || '初始化编辑器失败');
      }
    }

    rafIdRef.current = requestAnimationFrame(() => {
      loadLSF();
    });

    return () => {
      cleanup();
    };
  }, [yourConfig, yourTask]);

  return (
    <div className="editor-container">
      {editorReady ? (
        <div
          ref={editorContainerRef}
          className="w-full h-full flex flex-col"
        />
      ) : (
        <div>加载中...</div>
      )}
    </div>
  );
};

4. 组件样式(.module.scss)

.root {
  display: flex;
  flex-direction: column;
  height: 100%;
  width: 100%;

  // LabelStudio 样式覆盖
  :global(.lsf-wrapper) {
    flex: 1;
    display: flex;
    flex-direction: column;
  }

  :global(.lsf-editor) {
    min-width: 320px;
    width: 100%;
  }

  // 修复 relations-overlay 橙色遮罩问题
  :global(.relations-overlay) {
    background: transparent !important;
    fill: none !important;
  }

  :global(.lsf-container) {
    background: transparent !important;
  }
}

.editorContainer {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  width: 100%;
}

常见问题和解决方案

问题 1:样式完全不显示

症状

  • 标签按钮没有颜色
  • 按钮没有边框和背景
  • 整体看起来像是没有加载 CSS

原因

  • Webpack 配置缺少 CSS 模块的 lsf- 前缀处理
  • HTML 元素有 lsf-label 类名,但 CSS 只有 .label 选择器

解决方案

  1. 在 webpack.config.js 中添加 CSS 模块配置(见上文)
  2. 重启开发服务器(必须重启!)
  3. 硬刷新浏览器(Ctrl+Shift+R)

验证方法

# 1. 检查 HTML 元素
<div class="lsf-label lsf-label_selected">Positive</div>

# 2. 检查 CSS 文件(Network 标签)
.lsf-label {
  /* 样式定义 */
}

# 3. 两者应该匹配

问题 2:橙色到粉色的渐变遮罩

症状

  • 整个编辑器被一个橙色渐变覆盖
  • 无法点击或交互

原因

  • relations-overlay 是 LabelStudio 的关系标注覆盖层(SVG)
  • 默认有一个背景色

解决方案: 在组件样式中添加:

:global(.relations-overlay) {
  background: transparent !important;
  fill: none !important;
}

:global(.lsf-container) {
  background: transparent !important;
}

问题 3:静态导入 editor 样式导致冲突

症状

  • 样式加载但不生效
  • 或者样式冲突

原因

  • 静态导入 editor/src/assets/styles/global.scss 会导致样式加载顺序问题
  • 与 Tailwind CSS 冲突

解决方案

  • 不要静态导入 editor 的全局样式
  • 让样式随动态导入自动加载
  • 参考 playground 和 labelstudio 应用的实现

问题 4:TypeScript 类型错误

症状

Could not find a declaration file for module '@humansignal/editor'

解决方案: 在 webpack 配置中添加:

withNx({
  skipTypeChecking: true,
})

问题 5:音频解码器错误

症状

Can't resolve '@humansignal/audio-file-decoder'

解决方案

  1. 创建空模块:

    // src/utils/empty-module.js
    module.exports = {};
    
  2. 在 webpack 配置中添加别名:

    config.resolve = {
    ...config.resolve,
    alias: {
    ...config.resolve.alias,
    '@humansignal/audio-file-decoder': require.resolve('./src/utils/empty-module.js'),
    },
    };
    

调试技巧

1. 使用浏览器开发者工具

检查 HTML 元素

右键点击元素 → 检查
查看类名:应该是 lsf-label, lsf-button 等

检查 CSS 文件

Network 标签 → 找到 libs_editor_src_index_js.css
搜索 .lsf-label
应该能找到样式定义

检查样式应用

Elements 标签 → Styles 面板
查看 .lsf-label 的样式是否被应用
检查是否有被覆盖的样式

2. 控制台日志

在组件中添加详细日志:

console.log('开始加载 LabelStudio 编辑器...');
console.log('LabelStudio 加载成功');
console.log('初始化 LabelStudio 实例...');
console.log('Storage 初始化完成');
console.log('Annotation 创建成功');
console.log('Annotation 更新:', result);

3. 创建测试页面

创建一个独立的测试页面来隔离问题:

// src/views/editor-test/editor-test.tsx
// 使用简单的配置和数据
// 添加调试面板显示状态和错误

最佳实践

1. 参考成功案例

Playground 应用(推荐参考):

  • 简单的编辑器集成
  • 清晰的组件结构
  • 位置:web/apps/playground/src/components/PreviewPanel/

LabelStudio 应用(完整实现):

  • 完整的 Label Studio 应用
  • 复杂的功能集成
  • 位置:web/apps/labelstudio/

2. 样式隔离

使用 CSS 模块和 :global() 选择器:

.root {
  // 组件自己的样式
  
  // LabelStudio 样式覆盖
  :global(.lsf-wrapper) {
    // 全局样式
  }
}

3. 清理逻辑

始终实现完整的清理逻辑:

function cleanup() {
  // 清除 window.LabelStudio
  // 销毁编辑器实例
  // 取消动画帧
  // 移除事件监听器
}

useEffect(() => {
  // 初始化逻辑
  return () => {
    cleanup();
  };
}, [dependencies]);

4. 错误处理

提供友好的错误提示:

const [error, setError] = useState<string | null>(null);

try {
  // 加载和初始化
} catch (err: any) {
  console.error('Error loading LabelStudio:', err);
  setError(err.message || '初始化编辑器失败');
}

// 在 UI 中显示错误
{error && (
  <div className="error-message">
    {error}
  </div>
)}

配置检查清单

在集成 LabelStudio 之前,确保:

  • Webpack 配置添加了 CSS 前缀处理
  • 定义了 CSS_PREFIX 环境变量
  • 配置了 CSS 模块的 localIdentName
  • main.tsx 只导入 UI 库样式
  • 没有静态导入 editor 的全局样式
  • 使用动态导入 import('@humansignal/editor')
  • 实现了完整的清理逻辑
  • 添加了 relations-overlay 透明背景修复
  • 配置了 WebAssembly 支持
  • 添加了 Node.js 模块 fallback
  • 处理了音频解码器问题

故障排除流程

  1. 检查 webpack 配置

    • 确认 CSS 前缀配置已添加
    • 确认服务器已重启
  2. 检查样式加载

    • 打开 Network 标签
    • 确认 libs_editor_src_index_js.css 已加载
    • 检查文件大小(应该是几 MB)
  3. 检查类名匹配

    • HTML 元素的类名应该以 lsf- 开头
    • CSS 文件中的选择器也应该以 lsf- 开头
  4. 清除缓存

    • 硬刷新浏览器(Ctrl+Shift+R)
    • 清除浏览器缓存
    • 重启开发服务器
  5. 查看控制台

    • 检查是否有 JavaScript 错误
    • 检查是否有 CSS 加载错误
    • 查看自定义日志输出

相关资源

总结

集成 LabelStudio 编辑器的关键是:

  1. 正确的 Webpack 配置:添加 CSS 模块的 lsf- 前缀处理
  2. 正确的样式导入:只导入 UI 库样式,让编辑器样式自动加载
  3. 动态导入编辑器:使用 import('@humansignal/editor')
  4. 样式修复:添加 relations-overlay 透明背景
  5. 参考成功案例:遵循 playground 和 labelstudio 的实现方式

遵循这些规范,可以避免大部分常见问题,快速集成 LabelStudio 编辑器!