«

语音转文字Demo

六思逸 发布于 阅读:4 HTML


语音转文字Demo

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue 语音转文本 Demo</title>
    <style>
      :root {
        --bg: #f3efe8;
        --bg-strong: #fffaf2;
        --panel: rgba(255, 250, 243, 0.86);
        --panel-strong: #fffdf9;
        --text: #1f1a14;
        --muted: #6c6358;
        --line: rgba(79, 62, 43, 0.16);
        --accent: #c8541a;
        --accent-strong: #8f3608;
        --info: #176f6a;
        --info-soft: rgba(23, 111, 106, 0.12);
        --success: #1f7c57;
        --success-soft: rgba(31, 124, 87, 0.12);
        --warning: #ac5c08;
        --warning-soft: rgba(172, 92, 8, 0.12);
        --danger: #b03c2d;
        --danger-soft: rgba(176, 60, 45, 0.12);
        --shadow: 0 28px 70px rgba(69, 45, 18, 0.14);
        --radius-xl: 30px;
        --radius-lg: 20px;
        --radius-md: 14px;
        --radius-sm: 12px;
      }

      * {
        box-sizing: border-box;
      }

      html,
      body {
        margin: 0;
        min-height: 100%;
      }

      body {
        font-family: Aptos, "Microsoft YaHei UI", "Segoe UI", sans-serif;
        color: var(--text);
        background:
          radial-gradient(circle at top left, rgba(255, 190, 118, 0.35), transparent 30%),
          radial-gradient(circle at 85% 20%, rgba(23, 111, 106, 0.16), transparent 24%),
          linear-gradient(135deg, #efe7dc 0%, var(--bg) 44%, #faf6ee 100%);
        padding: 28px 16px 40px;
      }

      button,
      textarea {
        font: inherit;
      }

      .app-shell {
        max-width: 1180px;
        margin: 0 auto;
        display: grid;
        grid-template-columns: minmax(300px, 430px) minmax(320px, 1fr);
        gap: 24px;
      }

      .card {
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: var(--radius-xl);
        box-shadow: var(--shadow);
        backdrop-filter: blur(18px);
      }

      .intro {
        padding: 28px;
        position: sticky;
        top: 22px;
        overflow: hidden;
      }

      .intro::after {
        content: "";
        position: absolute;
        inset: auto -24px -40px auto;
        width: 180px;
        height: 180px;
        border-radius: 50%;
        background: radial-gradient(circle, rgba(200, 84, 26, 0.18), transparent 68%);
        pointer-events: none;
      }

      .eyebrow {
        display: inline-flex;
        align-items: center;
        gap: 10px;
        padding: 8px 14px;
        border-radius: 999px;
        background: rgba(200, 84, 26, 0.1);
        color: var(--accent-strong);
        font-size: 13px;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
      }

      .eyebrow::before {
        content: "";
        width: 10px;
        height: 10px;
        border-radius: 50%;
        background: currentColor;
        box-shadow: 0 0 0 5px rgba(200, 84, 26, 0.14);
      }

      h1 {
        margin: 18px 0 14px;
        font-size: clamp(34px, 6vw, 58px);
        line-height: 0.94;
        letter-spacing: -0.03em;
      }

      .intro-copy,
      .note-list,
      .support-copy,
      .helper {
        color: var(--muted);
      }

      .intro-copy {
        margin: 0 0 22px;
        font-size: 15px;
        line-height: 1.72;
      }

      .note-list {
        display: grid;
        gap: 12px;
        margin: 0;
        padding: 0;
        list-style: none;
      }

      .note-item {
        padding: 14px 16px;
        border-radius: var(--radius-lg);
        background: rgba(255, 255, 255, 0.58);
        border: 1px solid rgba(79, 62, 43, 0.1);
        line-height: 1.65;
        font-size: 14px;
      }

      .workspace {
        padding: 24px;
      }

      .status-bar {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
        gap: 16px;
        margin-bottom: 20px;
      }

      .status-pill {
        display: inline-flex;
        align-items: center;
        gap: 10px;
        padding: 10px 14px;
        border-radius: 999px;
        font-size: 13px;
        font-weight: 700;
        background: var(--info-soft);
        color: var(--info);
      }

      .status-pill::before {
        content: "";
        width: 10px;
        height: 10px;
        border-radius: 50%;
        background: currentColor;
        box-shadow: 0 0 0 5px rgba(23, 111, 106, 0.12);
      }

      .status-pill.listening {
        background: rgba(200, 84, 26, 0.12);
        color: var(--accent-strong);
      }

      .status-pill.stopped {
        background: var(--success-soft);
        color: var(--success);
      }

      .status-pill.unsupported,
      .status-pill.permission-denied,
      .status-pill.error,
      .status-pill.no-speech {
        background: var(--danger-soft);
        color: var(--danger);
      }

      .support-copy {
        max-width: 320px;
        font-size: 13px;
        line-height: 1.6;
        text-align: right;
      }

      .panel-grid {
        display: grid;
        gap: 18px;
      }

      .callout {
        display: grid;
        gap: 8px;
        padding: 18px;
        border-radius: var(--radius-lg);
        background: linear-gradient(135deg, rgba(255, 255, 255, 0.74), rgba(255, 250, 243, 0.94));
        border: 1px solid rgba(79, 62, 43, 0.12);
      }

      .callout strong {
        font-size: 15px;
      }

      .callout p {
        margin: 0;
        color: var(--muted);
        line-height: 1.68;
        font-size: 14px;
      }

      .callout.warning {
        background: linear-gradient(135deg, rgba(255, 243, 226, 0.92), rgba(255, 250, 243, 0.98));
        border-color: rgba(172, 92, 8, 0.18);
      }

      .controls {
        display: grid;
        gap: 16px;
        padding: 20px;
        border-radius: var(--radius-lg);
        background: var(--panel-strong);
        border: 1px solid var(--line);
      }

      .controls-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 16px;
      }

      .controls-head h2,
      .result-head h2 {
        margin: 0;
        font-size: 20px;
      }

      .helper {
        font-size: 13px;
        line-height: 1.6;
      }

      .button-row {
        display: flex;
        flex-wrap: wrap;
        gap: 12px;
      }

      .btn {
        border: 0;
        border-radius: 999px;
        padding: 12px 18px;
        cursor: pointer;
        transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
      }

      .btn:hover:not(:disabled) {
        transform: translateY(-1px);
      }

      .btn:focus-visible,
      .result-area:focus-visible {
        outline: 3px solid rgba(23, 111, 106, 0.24);
        outline-offset: 2px;
      }

      .btn:disabled {
        cursor: not-allowed;
        opacity: 0.5;
        box-shadow: none;
      }

      .btn-primary {
        background: linear-gradient(135deg, var(--accent), var(--accent-strong));
        color: #fff9f1;
        box-shadow: 0 16px 28px rgba(143, 54, 8, 0.22);
      }

      .btn-secondary {
        background: rgba(79, 62, 43, 0.08);
        color: var(--text);
      }

      .btn-ghost {
        background: rgba(23, 111, 106, 0.08);
        color: var(--info);
      }

      .meter-row {
        display: grid;
        grid-template-columns: 180px 1fr;
        gap: 18px;
        align-items: center;
      }

      .pulse {
        position: relative;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 108px;
        height: 108px;
        border-radius: 50%;
        background: radial-gradient(circle, rgba(200, 84, 26, 0.18), rgba(200, 84, 26, 0.04));
      }

      .pulse::before,
      .pulse::after {
        content: "";
        position: absolute;
        inset: 0;
        border-radius: 50%;
        border: 1px solid rgba(200, 84, 26, 0.18);
        opacity: 0;
      }

      .pulse.active::before {
        animation: ping 1.8s ease-out infinite;
      }

      .pulse.active::after {
        animation: ping 1.8s ease-out 0.6s infinite;
      }

      .pulse-core {
        width: 42px;
        height: 42px;
        border-radius: 50%;
        background: linear-gradient(135deg, var(--accent), var(--accent-strong));
        box-shadow: 0 10px 22px rgba(143, 54, 8, 0.26);
      }

      .meter-copy {
        display: grid;
        gap: 8px;
      }

      .meter-copy strong {
        font-size: 16px;
      }

      .meter-copy p {
        margin: 0;
        color: var(--muted);
        line-height: 1.65;
        font-size: 14px;
      }

      .error-box {
        padding: 14px 16px;
        border-radius: var(--radius-md);
        background: rgba(176, 60, 45, 0.08);
        border: 1px solid rgba(176, 60, 45, 0.18);
        color: var(--danger);
        line-height: 1.65;
        font-size: 14px;
      }

      .result-card {
        display: grid;
        gap: 14px;
        padding: 20px;
        border-radius: var(--radius-lg);
        background: var(--panel-strong);
        border: 1px solid var(--line);
      }

      .result-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
      }

      .result-meta {
        font-size: 13px;
        color: var(--muted);
      }

      .result-area {
        width: 100%;
        min-height: 280px;
        resize: vertical;
        padding: 16px 18px;
        border-radius: var(--radius-md);
        border: 1px solid rgba(79, 62, 43, 0.16);
        background: rgba(255, 255, 255, 0.8);
        color: var(--text);
        line-height: 1.72;
        transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
      }

      .result-area:focus {
        border-color: rgba(23, 111, 106, 0.46);
        box-shadow: 0 0 0 4px rgba(23, 111, 106, 0.1);
        transform: translateY(-1px);
      }

      .result-area::placeholder {
        color: #8d8378;
      }

      .result-toolbar {
        display: flex;
        flex-wrap: wrap;
        gap: 12px;
        align-items: center;
        justify-content: space-between;
      }

      .micro-copy {
        font-size: 13px;
        color: var(--muted);
        line-height: 1.6;
      }

      .copy-feedback {
        min-height: 20px;
        font-size: 13px;
        color: var(--success);
      }

      .copy-feedback.error {
        color: var(--danger);
      }

      .footer-note {
        font-size: 13px;
        line-height: 1.6;
        color: var(--muted);
      }

      @keyframes ping {
        0% {
          transform: scale(1);
          opacity: 0.72;
        }

        100% {
          transform: scale(1.4);
          opacity: 0;
        }
      }

      @media (max-width: 980px) {
        .app-shell {
          grid-template-columns: 1fr;
        }

        .intro {
          position: static;
        }

        .support-copy {
          text-align: left;
        }
      }

      @media (max-width: 720px) {
        body {
          padding: 16px 12px 30px;
        }

        .workspace,
        .intro {
          padding: 18px;
        }

        .status-bar,
        .controls-head,
        .result-head,
        .meter-row {
          grid-template-columns: 1fr;
          display: grid;
        }

        .meter-row {
          gap: 14px;
          justify-items: start;
        }

        .pulse {
          width: 92px;
          height: 92px;
        }

        h1 {
          font-size: 40px;
        }
      }
    </style>
  </head>
  <body>
    <div id="app" class="app-shell">
      <aside class="card intro">
        <div class="eyebrow">Vue Speech Demo</div>
        <h1>把中文语音直接转成可编辑文本</h1>
        <p class="intro-copy">
          这是一个独立的 HTML 单页 Demo,使用 Vue 3 CDN 和浏览器原生语音识别能力完成实时转写。它适合桌面 Chrome
          和 Edge 的快速演示,不依赖后端,也不会修改你现有的页面结构。
        </p>

        <ul class="note-list">
          <li class="note-item">
            默认识别语言为 <code>zh-CN</code>,只在拿到最终识别结果后追加到结果框,不显示临时抖动文本。
          </li>
          <li class="note-item">
            首次使用时,浏览器会请求麦克风权限。如果被拒绝,页面会进入明确的错误态并给出恢复提示。
          </li>
          <li class="note-item">
            这个版本聚焦“能演示、能录、能复制、状态清楚”,方便你后续接聊天输入框或 AI 接口。
          </li>
        </ul>
      </aside>

      <main class="card workspace">
        <div class="status-bar">
          <div :class="['status-pill', statusClass]">{{ statusText }}</div>
          <div class="support-copy">
            推荐浏览器: 桌面 Chrome / Edge
            <br />
            当前模式: 浏览器原生语音识别
          </div>
        </div>

        <div class="panel-grid">
          <section :class="['callout', browserSupported ? '' : 'warning']" aria-live="polite">
            <strong>{{ browserSupported ? '兼容性与权限提示' : '当前浏览器不支持语音识别' }}</strong>
            <p>{{ statusDetail }}</p>
          </section>

          <section class="controls" aria-labelledby="controlTitle">
            <div class="controls-head">
              <h2 id="controlTitle">录音控制区</h2>
              <div class="helper">开始识别后你可以连续说话,再手动停止并查看本轮最终转写。</div>
            </div>

            <div class="meter-row">
              <div :class="['pulse', isListening ? 'active' : '']" aria-hidden="true">
                <div class="pulse-core"></div>
              </div>

              <div class="meter-copy">
                <strong>{{ isListening ? '正在监听麦克风' : '等待开始识别' }}</strong>
                <p>
                  {{ isListening
                    ? '浏览器正在捕获语音并等待最终结果。你可以自然停顿,也可以点击“停止识别”结束本轮。'
                    : '点击“开始识别”后,页面会请求浏览器麦克风权限,并在本页将最终文本追加到结果区。' }}
                </p>
              </div>
            </div>

            <div class="button-row">
              <button class="btn btn-primary" type="button" @click="startRecognition" :disabled="!canStart">
                开始识别
              </button>
              <button class="btn btn-secondary" type="button" @click="stopRecognition" :disabled="!canStop">
                停止识别
              </button>
              <button class="btn btn-secondary" type="button" @click="clearTranscript">
                清空文本
              </button>
              <button class="btn btn-ghost" type="button" @click="copyTranscript" :disabled="!transcript.trim()">
                复制结果
              </button>
            </div>

            <div v-if="errorMessage" class="error-box" role="alert" aria-live="assertive">
              {{ errorMessage }}
            </div>
          </section>

          <section class="result-card" aria-labelledby="resultTitle">
            <div class="result-head">
              <h2 id="resultTitle">转写结果区</h2>
              <div class="result-meta">{{ transcriptStats }}</div>
            </div>

            <textarea
              v-model="transcript"
              class="result-area"
              spellcheck="false"
              placeholder="识别完成后的文本会追加到这里。你也可以手动编辑这段内容。"
              aria-label="语音转文本结果"
            ></textarea>

            <div class="result-toolbar">
              <div class="micro-copy">
                结果区支持手动补充和修改。连续多轮识别时,新文本会按行追加,避免覆盖之前的内容。
              </div>
              <div :class="['copy-feedback', copyState === 'error' ? 'error' : '']">{{ copyMessage }}</div>
            </div>

            <div class="footer-note">
              如果你后续要把它接进聊天场景,可以直接把这里的 `transcript` 状态绑定到消息输入框。
            </div>
          </section>
        </div>
      </main>
    </div>

    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <script>
      const { createApp } = Vue;

      createApp({
        data() {
          return {
            recognition: null,
            browserSupported: false,
            isListening: false,
            transcript: "",
            statusKey: "initial",
            statusText: "未开始",
            statusDetail: "点击“开始识别”即可使用浏览器麦克风转写中文语音。首次使用时请允许麦克风权限。",
            errorMessage: "",
            copyMessage: "",
            copyState: "idle",
            manualStopRequested: false,
            sessionHasResult: false,
            lastErrorCode: ""
          };
        },
        computed: {
          canStart() {
            return this.browserSupported && !this.isListening;
          },
          canStop() {
            return this.browserSupported && this.isListening;
          },
          statusClass() {
            if (this.statusKey === "listening") {
              return "listening";
            }

            if (this.statusKey === "stopped") {
              return "stopped";
            }

            if (
              this.statusKey === "unsupported" ||
              this.statusKey === "permission-denied" ||
              this.statusKey === "error" ||
              this.statusKey === "no-speech"
            ) {
              return this.statusKey;
            }

            return "";
          },
          transcriptStats() {
            const trimmed = this.transcript.trim();
            const charCount = trimmed.length;

            if (!charCount) {
              return "还没有文本";
            }

            const lineCount = trimmed.split(/\n+/).filter(Boolean).length;
            return `共 ${charCount} 个字符 · ${lineCount} 段内容`;
          }
        },
        mounted() {
          this.initRecognition();
        },
        beforeUnmount() {
          if (this.recognition && this.isListening) {
            this.recognition.stop();
          }
        },
        methods: {
          initRecognition() {
            const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

            if (!SpeechRecognition) {
              this.browserSupported = false;
              this.setStatus(
                "unsupported",
                "浏览器不支持",
                "当前浏览器没有提供 SpeechRecognition 能力。请使用桌面 Chrome 或 Edge 打开这个页面。"
              );
              this.errorMessage = "无法初始化语音识别。建议切换到最新版本的桌面 Chrome 或 Edge。";
              return;
            }

            const recognition = new SpeechRecognition();
            recognition.lang = "zh-CN";
            recognition.continuous = true;
            recognition.interimResults = false;
            recognition.maxAlternatives = 1;

            recognition.onstart = () => {
              this.isListening = true;
              this.sessionHasResult = false;
              this.lastErrorCode = "";
              this.copyMessage = "";
              this.copyState = "idle";
              this.errorMessage = "";
              this.setStatus(
                "listening",
                "识别中",
                "浏览器正在监听你的麦克风。保持清晰发音,完成后点击“停止识别”。"
              );
            };

            recognition.onresult = (event) => {
              let finalText = "";

              for (let index = event.resultIndex; index < event.results.length; index += 1) {
                const result = event.results[index];
                const transcriptPart = result && result[0] && typeof result[0].transcript === "string"
                  ? result[0].transcript
                  : "";

                if (result.isFinal && transcriptPart.trim()) {
                  finalText += transcriptPart.trim();
                }
              }

              if (finalText) {
                this.sessionHasResult = true;
                this.appendTranscript(finalText);
                this.setStatus(
                  "listening",
                  "识别中",
                  "已拿到新的最终结果。你可以继续说话,也可以现在停止本轮识别。"
                );
              }
            };

            recognition.onerror = (event) => {
              if (event.error === "aborted" && this.manualStopRequested) {
                return;
              }

              this.lastErrorCode = event.error || "unknown";
              this.isListening = false;
              this.manualStopRequested = false;

              const errorMap = {
                "not-allowed": {
                  key: "permission-denied",
                  text: "麦克风权限被拒绝",
                  detail: "浏览器没有获得麦克风访问权限。请允许权限后刷新页面或重新尝试。",
                  message: "请在地址栏或站点权限设置中允许麦克风访问,然后再次点击“开始识别”。"
                },
                "service-not-allowed": {
                  key: "permission-denied",
                  text: "语音服务不可用",
                  detail: "浏览器拒绝了语音识别服务访问。通常与权限设置或系统限制有关。",
                  message: "请确认浏览器和系统没有禁用语音服务,并允许麦克风权限。"
                },
                "audio-capture": {
                  key: "error",
                  text: "未检测到麦克风",
                  detail: "浏览器没有发现可用的录音设备,请检查麦克风是否连接或被其他程序占用。",
                  message: "请确认当前电脑已经连接并启用了麦克风设备。"
                },
                "network": {
                  key: "error",
                  text: "识别服务异常",
                  detail: "浏览器原生语音识别服务暂时不可用,请检查网络后再试。",
                  message: "网络异常会影响浏览器语音识别,稍后重试通常可以恢复。"
                },
                "no-speech": {
                  key: "no-speech",
                  text: "未识别到语音",
                  detail: "本轮没有识别到清晰语音内容。请靠近麦克风并清楚说话后重新开始。",
                  message: "没有写入任何脏文本,你可以直接重新开始识别。"
                },
                "language-not-supported": {
                  key: "error",
                  text: "语言不受支持",
                  detail: "当前浏览器不支持 zh-CN 语音识别配置。",
                  message: "请切换到支持中文语音识别的桌面 Chrome 或 Edge。"
                }
              };

              const mapped =
                errorMap[event.error] || {
                  key: "error",
                  text: "识别出错",
                  detail: "浏览器语音识别过程中发生了未知错误,请稍后再试。",
                  message: `错误代码: ${event.error || "unknown"}`
                };

              this.setStatus(mapped.key, mapped.text, mapped.detail);
              this.errorMessage = mapped.message;
            };

            recognition.onend = () => {
              const hadError = Boolean(this.lastErrorCode);
              this.isListening = false;

              if (hadError) {
                this.lastErrorCode = "";
                return;
              }

              if (this.manualStopRequested) {
                this.manualStopRequested = false;

                if (this.sessionHasResult) {
                  this.setStatus(
                    "stopped",
                    "已停止",
                    "本轮识别已结束,最终文本已经追加到结果区。你可以继续编辑,或再次开始新的录音。"
                  );
                } else {
                  this.setStatus(
                    "no-speech",
                    "未识别到语音",
                    "你结束了本轮识别,但浏览器没有返回任何最终文本。请靠近麦克风重新尝试。"
                  );
                }

                return;
              }

              if (this.sessionHasResult) {
                this.setStatus(
                  "stopped",
                  "已停止",
                  "浏览器已自动结束当前识别,本轮最终文本已经保留在结果区。"
                );
              } else {
                this.setStatus(
                  "no-speech",
                  "未识别到语音",
                  "浏览器结束了本轮识别,但没有获取到最终文本。请重新开始并保持清晰发音。"
                );
              }
            };

            this.recognition = recognition;
            this.browserSupported = true;
            this.setStatus(
              "initial",
              "未开始",
              "你的浏览器已具备语音识别能力。点击“开始识别”后,允许麦克风权限即可开始中文转写。"
            );
          },
          setStatus(key, text, detail) {
            this.statusKey = key;
            this.statusText = text;
            this.statusDetail = detail;
          },
          appendTranscript(text) {
            const normalized = text.replace(/\s+/g, " ").trim();

            if (!normalized) {
              return;
            }

            this.transcript = this.transcript.trim()
              ? `${this.transcript.trim()}\n${normalized}`
              : normalized;
          },
          startRecognition() {
            if (!this.browserSupported || !this.recognition || this.isListening) {
              return;
            }

            this.manualStopRequested = false;
            this.sessionHasResult = false;
            this.lastErrorCode = "";
            this.errorMessage = "";

            try {
              this.recognition.start();
            } catch (error) {
              this.setStatus(
                "error",
                "无法启动识别",
                "浏览器当前不能立即开始新一轮识别。请稍等一瞬后再次尝试。"
              );
              this.errorMessage = error instanceof Error ? error.message : "识别启动失败。";
            }
          },
          stopRecognition() {
            if (!this.recognition || !this.isListening) {
              return;
            }

            this.manualStopRequested = true;
            this.errorMessage = "";

            try {
              this.recognition.stop();
            } catch (error) {
              this.manualStopRequested = false;
              this.setStatus(
                "error",
                "停止失败",
                "浏览器没有成功结束当前识别,请稍后重试。"
              );
              this.errorMessage = error instanceof Error ? error.message : "停止识别失败。";
            }
          },
          clearTranscript() {
            this.transcript = "";
            this.copyMessage = "";
            this.copyState = "idle";

            if (this.browserSupported && !this.isListening && this.statusKey === "stopped") {
              this.setStatus(
                "initial",
                "未开始",
                "文本已清空。你可以点击“开始识别”,继续进行新的中文语音转写。"
              );
            }
          },
          async copyTranscript() {
            const content = this.transcript.trim();

            if (!content) {
              this.copyState = "error";
              this.copyMessage = "当前没有可复制的文本。";
              return;
            }

            try {
              if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
                await navigator.clipboard.writeText(content);
              } else {
                const tempTextarea = document.createElement("textarea");
                tempTextarea.value = content;
                tempTextarea.setAttribute("readonly", "readonly");
                tempTextarea.style.position = "fixed";
                tempTextarea.style.opacity = "0";
                document.body.appendChild(tempTextarea);
                tempTextarea.select();
                document.execCommand("copy");
                document.body.removeChild(tempTextarea);
              }

              this.copyState = "success";
              this.copyMessage = "识别结果已复制到剪贴板。";
            } catch (error) {
              this.copyState = "error";
              this.copyMessage = "复制失败,请手动选择文本后再复制。";
            }
          }
        }
      }).mount("#app");
    </script>
  </body>
</html>

语音 文字 语音转文字


扫描二维码,在手机上阅读
QQ: 712107091
微信: ityolo