瀏覽代碼

✨ feat(sdk-test): 添加实时字幕显示功能

- 创建CaptionDisplay组件实现字幕展示功能
- 添加字幕显示/隐藏切换按钮
- 实现多语言字幕切换功能(原文/翻译1/翻译2)
- 设计字幕显示区域样式,包含用户信息、时间戳和内容
- 限制最多显示5条最新字幕,支持自动滚动到底部
- 添加空状态提示和加载状态提示
- 在测试日志中记录转录和翻译结果
yourname 2 月之前
父節點
當前提交
efd789cb23
共有 3 個文件被更改,包括 333 次插入1 次删除
  1. 150 0
      src/pages/sdk-test/components/caption-display.tsx
  2. 88 1
      src/pages/sdk-test/index.module.scss
  3. 95 0
      src/pages/sdk-test/index.tsx

+ 150 - 0
src/pages/sdk-test/components/caption-display.tsx

@@ -0,0 +1,150 @@
+import { useEffect, useState, useRef, useMemo } from "react"
+import { Typography } from "antd"
+import styles from "../index.module.scss"
+
+const { Text } = Typography
+
+interface ITranslationItem {
+  lang: string
+  text: string
+}
+
+interface ICaptionData {
+  userName: string
+  content: string
+  translations?: ITranslationItem[]
+  timestamp: number
+}
+
+interface ICaptionDisplayProps {
+  visible?: boolean
+  captionLanguages?: string[]
+  sttData?: any
+}
+
+const CaptionDisplay = (props: ICaptionDisplayProps) => {
+  const { visible = true, captionLanguages = ["live"], sttData = {} } = props
+  const [captionList, setCaptionList] = useState<ICaptionData[]>([])
+  const captionRef = useRef<HTMLDivElement>(null)
+
+  // 监听STT数据变化,更新字幕列表
+  useEffect(() => {
+    if (sttData.transcribe1 || sttData.transcribe2) {
+      const newCaption: ICaptionData = {
+        userName: sttData.userName || "用户",
+        content: sttData.transcribe1 || sttData.transcribe2 || "",
+        translations: [],
+        timestamp: Date.now(),
+      }
+
+      // 处理翻译文本
+      if (sttData.translate1List?.length) {
+        sttData.translate1List.forEach((text: string, index: number) => {
+          if (text) {
+            newCaption.translations?.push({
+              lang: `translate${index + 1}`,
+              text,
+            })
+          }
+        })
+      }
+
+      if (sttData.translate2List?.length) {
+        sttData.translate2List.forEach((text: string, index: number) => {
+          if (text) {
+            newCaption.translations?.push({
+              lang: `translate${index + 2}`,
+              text,
+            })
+          }
+        })
+      }
+
+      setCaptionList((prev) => {
+        // 只保留最新的5条字幕
+        const newList = [...prev, newCaption]
+        return newList.slice(-5)
+      })
+    }
+  }, [sttData])
+
+  // 根据选择的语言过滤字幕内容
+  const filteredCaptionList = useMemo(() => {
+    return captionList.map((caption) => {
+      const filteredCaption: ICaptionData = {
+        ...caption,
+        translations: caption.translations?.filter((translation) =>
+          captionLanguages.includes(translation.lang),
+        ),
+      }
+
+      // 如果不显示原文,清空内容
+      if (!captionLanguages.includes("live")) {
+        filteredCaption.content = ""
+      }
+
+      return filteredCaption
+    })
+  }, [captionList, captionLanguages])
+
+  // 自动滚动到最新字幕
+  useEffect(() => {
+    if (captionRef.current) {
+      captionRef.current.scrollTop = captionRef.current.scrollHeight
+    }
+  }, [captionList])
+
+  if (!visible) {
+    return null
+  }
+
+  return (
+    <div className={styles.captionDisplay}>
+      <div className={styles.captionHeader}>
+        <Text strong>实时转录结果</Text>
+        <Text type="secondary">
+          {captionList.length > 0 ? `最新 ${captionList.length} 条` : "等待转录结果..."}
+        </Text>
+      </div>
+
+      <div className={styles.captionContainer} ref={captionRef}>
+        {filteredCaptionList.length === 0 ? (
+          <div className={styles.emptyCaption}>
+            <Text type="secondary">暂无转录内容,请开始语音转录...</Text>
+          </div>
+        ) : (
+          filteredCaptionList.map((item, index) => (
+            <div key={index} className={styles.captionItem}>
+              <div className={styles.captionMeta}>
+                <Text className={styles.userName}>{item.userName}:</Text>
+                <Text type="secondary" className={styles.timestamp}>
+                  {new Date(item.timestamp).toLocaleTimeString()}
+                </Text>
+              </div>
+
+              {item.content && (
+                <div className={styles.captionContent}>
+                  <Text>{item.content}</Text>
+                </div>
+              )}
+
+              {item.translations && item.translations.length > 0 && (
+                <div className={styles.translations}>
+                  {item.translations.map((translation, transIndex) => (
+                    <div key={transIndex} className={styles.translationItem}>
+                      <Text type="secondary">
+                        [{translation.lang}]: {translation.text}
+                      </Text>
+                    </div>
+                  ))}
+                </div>
+              )}
+            </div>
+          ))
+        )}
+      </div>
+    </div>
+  )
+}
+
+export default CaptionDisplay

+ 88 - 1
src/pages/sdk-test/index.module.scss

@@ -31,7 +31,8 @@
       gap: 24px;
 
       .configCard,
-      .testCard {
+      .testCard,
+      .captionCard {
         :global(.ant-card-body) {
           padding: 24px;
         }
@@ -90,4 +91,90 @@
       }
     }
   }
+
+  // 字幕显示组件样式
+  .captionDisplay {
+    border: 1px solid #d9d9d9;
+    border-radius: 6px;
+    background: #fafafa;
+    overflow: hidden;
+
+    .captionHeader {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 12px 16px;
+      background: #e6f7ff;
+      border-bottom: 1px solid #d9d9d9;
+
+      :global(.ant-typography) {
+        margin: 0;
+      }
+    }
+
+    .captionContainer {
+      height: 200px;
+      overflow-y: auto;
+      padding: 12px 16px;
+
+      .emptyCaption {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 100%;
+        color: #999;
+      }
+
+      .captionItem {
+        margin-bottom: 16px;
+        padding: 12px;
+        background: white;
+        border-radius: 4px;
+        border: 1px solid #f0f0f0;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+
+        .captionMeta {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          margin-bottom: 8px;
+
+          .userName {
+            font-weight: 600;
+            color: #1890ff;
+          }
+
+          .timestamp {
+            font-size: 12px;
+          }
+        }
+
+        .captionContent {
+          margin-bottom: 8px;
+          font-size: 14px;
+          line-height: 1.5;
+          color: #333;
+        }
+
+        .translations {
+          .translationItem {
+            margin-top: 4px;
+            padding: 4px 8px;
+            background: #f6ffed;
+            border-radius: 2px;
+            border-left: 2px solid #52c41a;
+            font-size: 12px;
+            line-height: 1.4;
+
+            &:first-child {
+              margin-top: 0;
+            }
+          }
+        }
+      }
+    }
+  }
 }

+ 95 - 0
src/pages/sdk-test/index.tsx

@@ -5,6 +5,8 @@ import {
   PlayCircleOutlined,
   StopOutlined,
   ReloadOutlined,
+  EyeOutlined,
+  EyeInvisibleOutlined,
 } from "@ant-design/icons"
 import { useNavigate } from "react-router-dom"
 import { genRandomUserId } from "@/common"
@@ -19,6 +21,9 @@ import type {
   IRtcManagerAdapter,
 } from "../../../packages/stt-sdk-core/src/types"
 
+// 导入字幕显示组件
+import CaptionDisplay from "./components/caption-display"
+
 const { Title, Text } = Typography
 
 const SdkTestPage = () => {
@@ -37,6 +42,8 @@ const SdkTestPage = () => {
   const [transcriptionStatus, setTranscriptionStatus] = useState<string>("idle")
   const [testResults, setTestResults] = useState<string[]>([])
   const [sttData, setSttData] = useState<any>({})
+  const [captionVisible, setCaptionVisible] = useState(true)
+  const [captionLanguages, setCaptionLanguages] = useState<string[]>(["live"])
 
   // 添加测试日志
   const addTestLog = (log: string) => {
@@ -57,6 +64,28 @@ const SdkTestPage = () => {
       setTranscriptionStatus("stopped")
       addTestLog("📢 转录状态更新: 转录已停止")
     }
+
+    // 如果有转录内容,记录到日志
+    if (data.transcribe1 || data.transcribe2) {
+      addTestLog(`📝 转录结果: ${data.transcribe1 || data.transcribe2}`)
+    }
+
+    // 如果有翻译内容,记录到日志
+    if (data.translate1List?.length) {
+      data.translate1List.forEach((text: string, index: number) => {
+        if (text) {
+          addTestLog(`🌐 翻译${index + 1}: ${text}`)
+        }
+      })
+    }
+
+    if (data.translate2List?.length) {
+      data.translate2List.forEach((text: string, index: number) => {
+        if (text) {
+          addTestLog(`🌐 翻译${index + 2}: ${text}`)
+        }
+      })
+    }
   }
 
   // 初始化SDK
@@ -274,6 +303,24 @@ const SdkTestPage = () => {
     }
   }
 
+  // 切换字幕显示
+  const toggleCaptionVisibility = () => {
+    setCaptionVisible(!captionVisible)
+    addTestLog(`📺 字幕显示: ${!captionVisible ? "开启" : "关闭"}`)
+  }
+
+  // 切换语言显示
+  const toggleLanguage = (language: string) => {
+    setCaptionLanguages((prev) => {
+      if (prev.includes(language)) {
+        return prev.filter((lang) => lang !== language)
+      } else {
+        return [...prev, language]
+      }
+    })
+    addTestLog(`🌐 语言显示: ${captionLanguages.includes(language) ? "关闭" : "开启"} ${language}`)
+  }
+
   // 返回主应用
   const goToMainApp = () => {
     nav("/home")
@@ -512,6 +559,54 @@ const SdkTestPage = () => {
               </Button>
             </Space>
           </Card>
+
+          <Card
+            title={
+              <Space>
+                <span>📺 实时转录显示</span>
+                <Button
+                  type="text"
+                  size="small"
+                  icon={captionVisible ? <EyeOutlined /> : <EyeInvisibleOutlined />}
+                  onClick={toggleCaptionVisibility}
+                >
+                  {captionVisible ? "隐藏" : "显示"}
+                </Button>
+              </Space>
+            }
+            className={styles.captionCard}
+            extra={
+              <Space>
+                <Button
+                  size="small"
+                  type={captionLanguages.includes("live") ? "primary" : "default"}
+                  onClick={() => toggleLanguage("live")}
+                >
+                  原文
+                </Button>
+                <Button
+                  size="small"
+                  type={captionLanguages.includes("translate1") ? "primary" : "default"}
+                  onClick={() => toggleLanguage("translate1")}
+                >
+                  翻译1
+                </Button>
+                <Button
+                  size="small"
+                  type={captionLanguages.includes("translate2") ? "primary" : "default"}
+                  onClick={() => toggleLanguage("translate2")}
+                >
+                  翻译2
+                </Button>
+              </Space>
+            }
+          >
+            <CaptionDisplay
+              visible={captionVisible}
+              captionLanguages={captionLanguages}
+              sttData={sttData}
+            />
+          </Card>
         </div>
       </div>
     </div>