|
|
@@ -42,18 +42,19 @@
|
|
|
z-index: 10001;
|
|
|
}
|
|
|
|
|
|
- .button {
|
|
|
- padding: 6px 12px;
|
|
|
- background-color: #007bff;
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
- border-radius: 5px;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 12px;
|
|
|
- margin: 0 4px;
|
|
|
- flex: 1;
|
|
|
- text-align: center;
|
|
|
- }
|
|
|
+ .button {
|
|
|
+ padding: 4px 6px;
|
|
|
+ background-color: #007bff;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 11px;
|
|
|
+ margin: 0 2px;
|
|
|
+ flex: 1;
|
|
|
+ text-align: center;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
|
|
|
.button-secondary {
|
|
|
background-color: #6c757d;
|
|
|
@@ -72,26 +73,30 @@
|
|
|
background-color: #dc3545;
|
|
|
}
|
|
|
|
|
|
- .button-pause {
|
|
|
- background-color: #ffc107;
|
|
|
- color: black;
|
|
|
- }
|
|
|
+ .button-pause {
|
|
|
+ background-color: #ffc107;
|
|
|
+ color: black;
|
|
|
+ }
|
|
|
+
|
|
|
+ .button-stop {
|
|
|
+ background-color: #dc3545;
|
|
|
+ }
|
|
|
|
|
|
/* 底部按钮栏 */
|
|
|
- .bottom-bar {
|
|
|
- position: fixed;
|
|
|
- bottom: 0;
|
|
|
- margin: auto;
|
|
|
- width: 60%;
|
|
|
- left: 50%;
|
|
|
- transform: translateX(-50%);
|
|
|
- background-color: rgba(255, 255, 255, 0.0);
|
|
|
- display: flex;
|
|
|
- justify-content: space-around;
|
|
|
- padding: 8px 0;
|
|
|
- box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
|
|
- z-index: 1000;
|
|
|
- }
|
|
|
+ .bottom-bar {
|
|
|
+ position: fixed;
|
|
|
+ bottom: 0;
|
|
|
+ margin: auto;
|
|
|
+ width: 92%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ background-color: rgba(255, 255, 255, 0.0);
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+ padding: 6px 0;
|
|
|
+ box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
|
|
+ z-index: 1000;
|
|
|
+ }
|
|
|
|
|
|
/* 顶部状态条 */
|
|
|
#loading-indicator {
|
|
|
@@ -154,17 +159,17 @@
|
|
|
}
|
|
|
|
|
|
/* 手机端优化 */
|
|
|
- @media (max-width: 768px) {
|
|
|
+ @media (max-width: 768px) {
|
|
|
.modal {
|
|
|
width: 95%;
|
|
|
padding: 10px;
|
|
|
}
|
|
|
|
|
|
- .button {
|
|
|
- font-size: 12px;
|
|
|
- padding: 6px 8px;
|
|
|
- margin: 0 2px;
|
|
|
- }
|
|
|
+ .button {
|
|
|
+ font-size: 10px;
|
|
|
+ padding: 4px 4px;
|
|
|
+ margin: 0 2px;
|
|
|
+ }
|
|
|
|
|
|
.bottom-bar {
|
|
|
padding: 6px 0;
|
|
|
@@ -183,13 +188,14 @@
|
|
|
|
|
|
<audio id="audio-player" controls></audio>
|
|
|
|
|
|
- <div class="bottom-bar">
|
|
|
- <button id="uploadButton" class="button">上传 PDF</button>
|
|
|
- <button id="openDirectoryButton" class="button">打开目录</button>
|
|
|
- <button id="select-all-text" class="button button-light">阅读整页</button>
|
|
|
- <button id="toggle-text-select" class="button button-success">文本选择</button>
|
|
|
- <button id="pause-play-button" class="button button-pause">暂停播放</button>
|
|
|
- </div>
|
|
|
+ <div class="bottom-bar">
|
|
|
+ <button id="uploadButton" class="button">上传 PDF</button>
|
|
|
+ <button id="openDirectoryButton" class="button">打开目录</button>
|
|
|
+ <button id="select-all-text" class="button button-light">阅读整页</button>
|
|
|
+ <button id="toggle-text-select" class="button button-success">文本选择</button>
|
|
|
+ <button id="pause-play-button" class="button button-pause">暂停播放</button>
|
|
|
+ <button id="stop-play-button" class="button button-stop">停止</button>
|
|
|
+ </div>
|
|
|
|
|
|
<input type="file" id="pdfInput" accept="application/pdf" style="display: none;" />
|
|
|
|
|
|
@@ -1039,15 +1045,18 @@
|
|
|
const directoryModal = document.getElementById('directoryModal');
|
|
|
const closeDirectoryButton = document.getElementById('closeDirectoryButton');
|
|
|
const pdfList = document.getElementById('pdfList');
|
|
|
- const prevPageButton = document.getElementById('prevPage');
|
|
|
- const nextPageButton = document.getElementById('nextPage');
|
|
|
- const pageInfo = document.getElementById('pageInfo');
|
|
|
- const pausePlayButton = document.getElementById('pause-play-button');
|
|
|
-
|
|
|
- let selectedFile = null;
|
|
|
- let currentPage = 1;
|
|
|
- const itemsPerPage = 10;
|
|
|
- let allFiles = [];
|
|
|
+ const prevPageButton = document.getElementById('prevPage');
|
|
|
+ const nextPageButton = document.getElementById('nextPage');
|
|
|
+ const pageInfo = document.getElementById('pageInfo');
|
|
|
+ const readPageButton = document.getElementById('select-all-text');
|
|
|
+ const pausePlayButton = document.getElementById('pause-play-button');
|
|
|
+ const stopPlayButton = document.getElementById('stop-play-button');
|
|
|
+
|
|
|
+ let selectedFile = null;
|
|
|
+ let currentPage = 1;
|
|
|
+ const itemsPerPage = 10;
|
|
|
+ let allFiles = [];
|
|
|
+ let selectedStartWord = '';
|
|
|
|
|
|
// 上传按钮点击事件
|
|
|
uploadButton.addEventListener('click', function () {
|
|
|
@@ -1225,10 +1234,12 @@
|
|
|
});
|
|
|
|
|
|
// 语音控制
|
|
|
- let isListening = false;
|
|
|
- let audioContext = null;
|
|
|
- let isPaused = false;
|
|
|
- let currentAudio = null;
|
|
|
+ let isListening = false;
|
|
|
+ let audioContext = null;
|
|
|
+ let isPaused = false;
|
|
|
+ let currentAudio = null;
|
|
|
+ let activePlayController = null;
|
|
|
+ let activeStreamReader = null;
|
|
|
|
|
|
// 初始化 AudioContext
|
|
|
function initAudioContext() {
|
|
|
@@ -1262,18 +1273,84 @@
|
|
|
button.classList.toggle('button-danger', isListening);
|
|
|
});
|
|
|
|
|
|
- // 暂停/继续播放
|
|
|
- pausePlayButton.addEventListener('click', () => {
|
|
|
- isPaused = !isPaused;
|
|
|
- pausePlayButton.textContent = isPaused ? "继续播放" : "暂停播放";
|
|
|
- if (isPaused && currentAudio) {
|
|
|
+ // 暂停/继续播放
|
|
|
+ pausePlayButton.addEventListener('click', () => {
|
|
|
+ isPaused = !isPaused;
|
|
|
+ pausePlayButton.textContent = isPaused ? "继续播放" : "暂停播放";
|
|
|
+ if (isPaused && currentAudio) {
|
|
|
currentAudio.pause();
|
|
|
if (audioContext) audioContext.suspend();
|
|
|
} else if (!isPaused && currentAudio) {
|
|
|
- currentAudio.play();
|
|
|
- if (audioContext) audioContext.resume();
|
|
|
- }
|
|
|
- });
|
|
|
+ currentAudio.play();
|
|
|
+ if (audioContext) audioContext.resume();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 停止播放
|
|
|
+ function stopCurrentPlayback(showTip = true) {
|
|
|
+ if (activePlayController) {
|
|
|
+ activePlayController.abort();
|
|
|
+ activePlayController = null;
|
|
|
+ }
|
|
|
+ if (activeStreamReader) {
|
|
|
+ try {
|
|
|
+ activeStreamReader.cancel();
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('取消流读取失败:', e);
|
|
|
+ }
|
|
|
+ activeStreamReader = null;
|
|
|
+ }
|
|
|
+ if (currentAudio) {
|
|
|
+ currentAudio.pause();
|
|
|
+ currentAudio.src = '';
|
|
|
+ currentAudio = null;
|
|
|
+ }
|
|
|
+ isPaused = false;
|
|
|
+ pausePlayButton.textContent = "暂停播放";
|
|
|
+ if (showTip) {
|
|
|
+ const loadingIndicator = document.getElementById('loading-indicator');
|
|
|
+ loadingIndicator.textContent = '已停止';
|
|
|
+ loadingIndicator.style.display = 'block';
|
|
|
+ setTimeout(() => {
|
|
|
+ loadingIndicator.style.display = 'none';
|
|
|
+ }, 800);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ stopPlayButton.addEventListener('click', () => {
|
|
|
+ stopCurrentPlayback();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 仅识别 PDF 页面文字层里的选择
|
|
|
+ function getSelectedWordOnPage() {
|
|
|
+ const selection = window.getSelection();
|
|
|
+ if (!selection || selection.rangeCount === 0) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ const selectedText = selection.toString().trim();
|
|
|
+ if (!selectedText) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ const range = selection.getRangeAt(0);
|
|
|
+ const container = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
|
+ ? range.commonAncestorContainer
|
|
|
+ : range.commonAncestorContainer.parentElement;
|
|
|
+ if (!container || !container.closest('.textLayer')) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ const firstWord = selectedText.split(/\s+/)[0].trim();
|
|
|
+ return firstWord.replace(/(^[^A-Za-z0-9\u4e00-\u9fa5]+|[^A-Za-z0-9\u4e00-\u9fa5]+$)/g, '') || firstWord;
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateReadPageButtonText() {
|
|
|
+ selectedStartWord = getSelectedWordOnPage();
|
|
|
+ readPageButton.textContent = selectedStartWord ? '从此阅读' : '阅读整页';
|
|
|
+ }
|
|
|
+ document.addEventListener('selectionchange', updateReadPageButtonText);
|
|
|
+ updateReadPageButtonText();
|
|
|
|
|
|
// 播放音频队列(添加句子间停顿以模拟段落停顿)
|
|
|
async function playAudioSequentially(queue, signal) {
|
|
|
@@ -1381,26 +1458,29 @@
|
|
|
queue.splice(index, 0, chunk);
|
|
|
}
|
|
|
|
|
|
- // 开始转换(发送完整文本,让TTS API处理分割)
|
|
|
- async function startConversion(fullText) {
|
|
|
- const loadingIndicator = document.getElementById('loading-indicator');
|
|
|
- try {
|
|
|
- const response = await fetch('http://141.140.15.30:8028/generate', {
|
|
|
- method: 'POST',
|
|
|
- headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({ text: fullText, voice: "af_heart", speed: 1.0 })
|
|
|
- });
|
|
|
+ // 开始转换(发送完整文本,让TTS API处理分割)
|
|
|
+ async function startConversion(fullText) {
|
|
|
+ const loadingIndicator = document.getElementById('loading-indicator');
|
|
|
+ try {
|
|
|
+ stopCurrentPlayback(false);
|
|
|
+ const response = await fetch('http://141.140.15.30:8028/generate', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ text: fullText, voice: "af_heart", speed: 1.0 })
|
|
|
+ });
|
|
|
|
|
|
if (!response.ok) throw new Error(`服务器错误: ${response.status}`);
|
|
|
|
|
|
- const reader = response.body.getReader();
|
|
|
- const decoder = new TextDecoder();
|
|
|
- let buffer = '';
|
|
|
- const audioQueue = [];
|
|
|
- let expectedIndex = 0;
|
|
|
-
|
|
|
- const playController = new AbortController();
|
|
|
- const playPromise = playAudioSequentially(audioQueue, playController.signal);
|
|
|
+ const reader = response.body.getReader();
|
|
|
+ activeStreamReader = reader;
|
|
|
+ const decoder = new TextDecoder();
|
|
|
+ let buffer = '';
|
|
|
+ const audioQueue = [];
|
|
|
+ let expectedIndex = 0;
|
|
|
+
|
|
|
+ const playController = new AbortController();
|
|
|
+ activePlayController = playController;
|
|
|
+ const playPromise = playAudioSequentially(audioQueue, playController.signal);
|
|
|
|
|
|
while (true) {
|
|
|
const { done, value } = await reader.read();
|
|
|
@@ -1431,14 +1511,21 @@
|
|
|
}
|
|
|
|
|
|
audioQueue.push(null); // 结束信号
|
|
|
- await playPromise;
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error(error);
|
|
|
- loadingIndicator.textContent = `错误: ${error.message}`;
|
|
|
- loadingIndicator.style.display = 'block';
|
|
|
- }
|
|
|
- }
|
|
|
+ await playPromise;
|
|
|
+ activePlayController = null;
|
|
|
+ activeStreamReader = null;
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ activePlayController = null;
|
|
|
+ activeStreamReader = null;
|
|
|
+ if (error.name === 'AbortError') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ console.error(error);
|
|
|
+ loadingIndicator.textContent = `错误: ${error.message}`;
|
|
|
+ loadingIndicator.style.display = 'block';
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
// 监听文本选择
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
@@ -1469,28 +1556,41 @@
|
|
|
});
|
|
|
|
|
|
// 阅读整页
|
|
|
- document.getElementById('select-all-text').addEventListener('click', async function () {
|
|
|
- const { pdfViewer } = PDFViewerApplication;
|
|
|
- const currentPageNumber = pdfViewer.currentPageNumber;
|
|
|
- const loadingIndicator = document.getElementById('loading-indicator');
|
|
|
-
|
|
|
- try {
|
|
|
+ readPageButton.addEventListener('click', async function () {
|
|
|
+ const { pdfViewer } = PDFViewerApplication;
|
|
|
+ const currentPageNumber = pdfViewer.currentPageNumber;
|
|
|
+ const loadingIndicator = document.getElementById('loading-indicator');
|
|
|
+
|
|
|
+ try {
|
|
|
initAudioContext();
|
|
|
await getPlayPermission();
|
|
|
loadingIndicator.textContent = '正在提取页面文本...';
|
|
|
loadingIndicator.style.display = 'block';
|
|
|
|
|
|
- const pdfDocument = PDFViewerApplication.pdfDocument;
|
|
|
- const page = await pdfDocument.getPage(currentPageNumber);
|
|
|
- const textContent = await page.getTextContent();
|
|
|
-
|
|
|
- // 提取完整文本,保留标点符号(不预分割)
|
|
|
- const fullText = textContent.items.map(item => item.str).join(' ').trim();
|
|
|
-
|
|
|
- if (fullText.length === 0) {
|
|
|
- loadingIndicator.textContent = '页面无文本内容';
|
|
|
- return;
|
|
|
- }
|
|
|
+ const pdfDocument = PDFViewerApplication.pdfDocument;
|
|
|
+ const page = await pdfDocument.getPage(currentPageNumber);
|
|
|
+ const textContent = await page.getTextContent();
|
|
|
+
|
|
|
+ // 提取完整文本,保留标点符号(不预分割)
|
|
|
+ let fullText = textContent.items.map(item => item.str).join(' ').trim();
|
|
|
+
|
|
|
+ const startWord = selectedStartWord || getSelectedWordOnPage();
|
|
|
+ if (startWord) {
|
|
|
+ const normalizedText = fullText.toLowerCase();
|
|
|
+ const normalizedStart = startWord.toLowerCase();
|
|
|
+ const startIndex = normalizedText.indexOf(normalizedStart);
|
|
|
+ if (startIndex >= 0) {
|
|
|
+ fullText = fullText.slice(startIndex).trim();
|
|
|
+ } else {
|
|
|
+ loadingIndicator.textContent = `未在当前页找到起始词:${startWord},将从页首阅读`;
|
|
|
+ loadingIndicator.style.display = 'block';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fullText.length === 0) {
|
|
|
+ loadingIndicator.textContent = '页面无文本内容';
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
await startConversion(fullText);
|
|
|
} catch (error) {
|