监听 edge大声朗读 样式变化

cpp 复制代码
<msreadoutspan class="msreadout-line-highlight msreadout-inactive-highlight">黛玉方进入房时,只见两个人搀着一位鬓发如银的老母迎上来,黛玉便<msreadoutspan class="msreadout-word-highlight">知</msreadoutspan>是他</msreadoutspan>



<msreadoutspan class="msreadout-line-highlight msreadout-inactive-highlight">黛玉一一拜见过。贾母又说:"请姑娘们来。今日远客才来,<msreadoutspan class="msreadout-word-highlight">可以</msreadoutspan>不必上学去</msreadoutspan>

<p>"血米放在陈家,又不会跑,何况族中还有着贺章族 <msreadoutspan class="msreadout-line-highlight"> 叔,家族随时都可 <msreadoutspan class="msreadout-word-highlight">动手</msreadoutspan> 。" </msreadoutspan> </p>

目的是将edge朗读的内容,发送到 有声小说书屋软件中,显示

方案一: 150ms内 的 发一次

cpp 复制代码
// 防止重复处理:改用文本内容作为 key(避免 DOM 元素重建导致重复)
const processedTexts = new Set();

// 判断是否是"行级"朗读片段
function isLineSpan(el) {
  return (
    el.tagName?.toLowerCase() === 'msreadoutspan' &&
    el.classList.contains('msreadout-line-highlight')
  );
}

// 全局变量:用于缓冲和定时器
let sentenceBuffer = [];
let sendTimer = null;
const SEND_DELAY = 150; // 合并窗口:150ms

// 判断两个字符串之间是否应加空格(仅当前后都是英文字母时)
function shouldAddSpace(prev, current) {
  if (!prev || !current) return false;
  const lastChar = prev[prev.length - 1];
  const firstChar = current[0];
  const isEnglishLetter = /[a-zA-Z]/;
  return isEnglishLetter.test(lastChar) && isEnglishLetter.test(firstChar);
}

// 处理一个朗读片段
function processLineSpan(span) {
  const fullText = span.textContent.trim();
  if (!fullText) return;

  // ✅ 关键:用文本内容防重(不是 DOM 元素)
  if (processedTexts.has(fullText)) {
    // console.log('⏭️ 跳过重复文本:', fullText);
    return;
  }

  // 添加到已处理集合
  processedTexts.add(fullText);

  // 防内存泄漏:只保留最近 50 条
  if (processedTexts.size > 50) {
    const arr = Array.from(processedTexts);
    processedTexts.clear();
    arr.slice(-30).forEach(t => processedTexts.add(t)); // 保留最近 30 条
  }

  console.log('📜 收到片段:', fullText);
  sentenceBuffer.push(fullText);

  // 重置发送定时器
  if (sendTimer) clearTimeout(sendTimer);
  sendTimer = setTimeout(() => {
    // 智能拼接句子
    let combinedText = '';
    for (let i = 0; i < sentenceBuffer.length; i++) {
      if (i === 0) {
        combinedText = sentenceBuffer[i];
      } else {
        if (shouldAddSpace(sentenceBuffer[i - 1], sentenceBuffer[i])) {
          combinedText += ' ' + sentenceBuffer[i];
        } else {
          combinedText += sentenceBuffer[i];
        }
      }
    }

    // 清空缓冲区
    sentenceBuffer = [];

    if (combinedText) {
      console.log('📤 发送完整句子:', combinedText);
      sendToTtsServer(combinedText);
    }
  }, SEND_DELAY);
}

// 发送文本到本地 TTS 服务
function sendToTtsServer(text) {
  fetch('http://127.0.0.1:8088/tts', {
    method: 'POST',
    body: text,
    headers: { 'Content-Type': 'text/plain; charset=utf-8' }
  })
  .then(response => response.ok ? response.text() : Promise.reject(response.status))
  .then(data => console.log('📡 服务器响应:', data))
  .catch(error => console.error('💥 网络错误:无法连接到"有声小说书屋"程序', error));
}

// 监听 DOM 变化
const observer = new MutationObserver(mutations => {
  for (const mutation of mutations) {
    if (mutation.type === 'childList') {
      for (const node of mutation.addedNodes) {
        if (node.nodeType === Node.ELEMENT_NODE) {
          // 如果新节点本身就是目标 span
          if (isLineSpan(node)) {
            processLineSpan(node);
          }
          // 如果新节点内部包含目标 span
          if (node.querySelectorAll) {
            try {
              node.querySelectorAll('msreadoutspan.msreadout-line-highlight').forEach(processLineSpan);
            } catch (e) {
              // 降级:某些环境可能不支持自定义标签选择器
              const walk = (el) => {
                if (isLineSpan(el)) processLineSpan(el);
                el.children?.forEach(walk);
              };
              walk(node);
            }
          }
        }
      }
    }
  }
});

// 扫描页面中已存在的朗读行
document.querySelectorAll('msreadoutspan.msreadout-line-highlight').forEach(processLineSpan);

// 开始监听整个页面
observer.observe(document.body, { childList: true, subtree: true });

console.log('监听页面朗读行(含嵌套)已启动...');

方案二; 直接发

cpp 复制代码
// 防止重复处理:记录已处理的外层 line span 元素
const processedLineSpans = new Set();

// 判断是否是"完整句子"的外层容器
function isLineContainer(el) {
  return (
    el.tagName?.toLowerCase() === 'msreadoutspan' &&
    el.classList.contains('msreadout-line-highlight')
  );
}

// 发送完整句子到 TTS 服务
function sendToTtsServer(text) {
  if (!text.trim()) return;
  fetch('http://127.0.0.1:8088/tts', {
    method: 'POST',
    body: text,
    headers: { 'Content-Type': 'text/plain; charset=utf-8' }
  })
  .then(response => response.ok ? response.text() : Promise.reject(response.status))
  .then(data => console.log('📡 TTS 发送成功:', data))
  .catch(error => console.error('💥 网络错误:无法连接到"有声小说书屋"程序', error));
}

// 处理一个完整的句子容器
function processLineContainer(span) {
  if (processedLineSpans.has(span)) return;
  processedLineSpans.add(span);

  // ✅ 关键:textContent 自动合并所有嵌套文本(忽略内部标签)
  const fullText = span.textContent.trim();
  if (fullText) {
    console.log('📤 发送完整句子:', fullText);
    sendToTtsServer(fullText);
  }
}

// MutationObserver:监听新插入的节点
const observer = new MutationObserver(mutations => {
  for (const mutation of mutations) {
    if (mutation.type === 'childList') {
      for (const node of mutation.addedNodes) {
        if (node.nodeType === Node.ELEMENT_NODE) {
          // 如果新节点本身就是 line 容器
          if (isLineContainer(node)) {
            processLineContainer(node);
          }
          // 如果新节点内部包含 line 容器(比如批量插入)
          if (node.querySelectorAll) {
            try {
              node.querySelectorAll('msreadoutspan.msreadout-line-highlight')
                .forEach(processLineContainer);
            } catch (e) {
              // 降级遍历(兼容性兜底)
              const walk = (el) => {
                if (isLineContainer(el)) processLineContainer(el);
                el.children?.forEach(walk);
              };
              walk(node);
            }
          }
        }
      }
    }
  }
});

// 扫描页面中已存在的完整句子(防止遗漏初始内容)
document.querySelectorAll('msreadoutspan.msreadout-line-highlight')
  .forEach(processLineContainer);

// 开始监听整个页面
observer.observe(document.body, { childList: true, subtree: true });

console.log('监听页面完整朗读行(一次性发送)已启动...');

方案三 直接发

目前采用的,

cpp 复制代码
// 防重:记录已处理的 line 容器(基于 DOM 元素)
const processedLines = new Set();

// 判断是否为"整句朗读容器"
function isLineSpan(el) {
  return (
    el?.tagName?.toLowerCase() === 'msreadoutspan' &&
    el.classList.contains('msreadout-line-highlight')
  );
}

// 发送文本到 TTS 服务
function sendToTtsServer(text) {
  const clean = text.trim();
  if (!clean) return;

  console.log('📤 发送完整句子:', clean);
  fetch('http://127.0.0.1:8088/tts', {
    method: 'POST',
    body: clean,
    headers: { 'Content-Type': 'text/plain; charset=utf-8' }
  })
  .then(res => res.ok ? res.text() : Promise.reject(res.status))
  .then(data => console.log('📡 服务器响应:', data))
  .catch(err => console.error('💥 TTS 服务连接失败:', err));
}

// 处理一个完整的朗读行
function handleLineSpan(span) {
  if (processedLines.has(span)) return;
  processedLines.add(span);

  // ✅ 自动合并所有嵌套文本(包括 msreadout-word-highlight)
  const fullText = span.textContent;
  sendToTtsServer(fullText);
}

// 监听 DOM 变化
const observer = new MutationObserver(mutations => {
  for (const mutation of mutations) {
    if (mutation.type !== 'childList') continue;
    for (const node of mutation.addedNodes) {
      if (node.nodeType !== Node.ELEMENT_NODE) continue;

      // 情况1:新节点本身就是 line 容器
      if (isLineSpan(node)) {
        handleLineSpan(node);
      }

      // 情况2:新节点内部包含 line 容器(如 <p> 包裹的 span)
      if (node.querySelectorAll) {
        try {
          // 优先用 querySelectorAll(高效)
          node.querySelectorAll('msreadoutspan.msreadout-line-highlight')
            .forEach(handleLineSpan);
        } catch (e) {
          // 兜底:递归遍历(兼容老旧环境)
          const walk = (el) => {
            if (isLineSpan(el)) handleLineSpan(el);
            el.children?.forEach(walk);
          };
          walk(node);
        }
      }
    }
  }
});

// 扫描页面初始内容(防止遗漏)
document.querySelectorAll('msreadoutspan.msreadout-line-highlight')
  .forEach(handleLineSpan);

// 启动监听
observer.observe(document.body, { childList: true, subtree: true });

console.log('监听页面朗读行(整句提取)已启动...');
相关推荐
油丶酸萝卜别吃2 小时前
修改chrome配置,关闭跨域校验
前端·chrome
m0_740043732 小时前
3、Vuex-Axios-Element UI
前端·javascript·vue.js
风止何安啊3 小时前
一场组件的进化脱口秀——React从 “类” 到 “hooks” 的 “改头换面”
前端·react.js·面试
JS_GGbond3 小时前
给数组装上超能力:JavaScript数组方法趣味指南
前端·javascript
前端无涯3 小时前
Tailwind CSS v4 开发 APP 内嵌 H5:安卓 WebView 样式丢失问题解决与降级实战
前端
小邋遢2.03 小时前
vscod 执行npm build报错:Error: Cannot find module ‘vite‘
前端·npm·node.js
是你的小橘呀3 小时前
新手入门 React 必备:电影榜单项目核心知识点全解析
前端·javascript
yinmaisoft3 小时前
JNPF 钉钉双向同步攻略:组织 / 用户一键打通,触发事件自动联动
前端·低代码·钉钉
梨子同志3 小时前
Node.js Buffer 和 Stream
前端