监听 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('监听页面朗读行(整句提取)已启动...');
相关推荐
崔庆才丨静觅14 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606114 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了14 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅15 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅15 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment15 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅16 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊16 小时前
jwt介绍
前端
爱敲代码的小鱼16 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax