speechify-clone/static/index.html
ifedan-ed e1ff5b045e Real PDF rendering with PDF.js and word-level highlights on canvas
- PDF pages rendered by PDF.js directly from File object (no conversion shown)
- Each page gets a transparent overlay canvas for drawing highlights
- Word positions extracted from PDF.js getTextContent() in PDF coordinate space
- convertToViewportPoint() maps PDF space → canvas pixels correctly (handles y-flip)
- On each sentence: purple highlight rectangles drawn over matching words on the PDF
- Sentence→word mapping via character-position search through word stream
- PDF rendering and text extraction run in parallel (pages appear as they render)
- Nextcloud PDFs use pdf_b64 → PDF.js rendering path
- Removed iframe approach entirely

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 05:16:01 +02:00

225 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Speechify</title>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
<link rel="stylesheet" href="/static/style.css"/>
</head>
<body>
<div class="app">
<!-- Top Bar -->
<header class="topbar">
<div class="topbar-left">
<div class="logo">
<svg width="30" height="30" viewBox="0 0 30 30" fill="none">
<rect width="30" height="30" rx="9" fill="#7c3aed"/>
<polygon points="11,8 23,15 11,22" fill="white"/>
</svg>
<span>Speechify</span>
</div>
</div>
<div class="topbar-right">
<button class="btn-icon" id="btn-sidebar-toggle" title="Toggle Input">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
</button>
<button class="btn-icon" id="btn-settings-toggle" title="Voice Settings">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
</button>
<div class="user-menu" id="user-menu">
<button class="user-btn" id="user-btn">
<span id="user-label"></span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="user-dropdown" id="user-dropdown">
<a href="/admin" class="dd-item" id="dd-admin">Admin Panel</a>
<a href="/settings" class="dd-item">Settings</a>
<button class="dd-item" onclick="logout()">Sign Out</button>
</div>
</div>
</div>
</header>
<!-- Email verification banner -->
<div class="verify-banner" id="verify-banner">
<span>⚠️ Please verify your email address. Check your inbox for a verification link.</span>
<button onclick="resendVerification()">Resend</button>
<button class="close-btn" onclick="this.parentElement.classList.remove('show')"></button>
</div>
<div class="main-layout">
<!-- Left Sidebar -->
<aside class="sidebar-left" id="sidebar-left">
<div class="sidebar-header"><h3>Add Content</h3></div>
<div class="tabs">
<button class="tab active" data-tab="text">Text</button>
<button class="tab" data-tab="pdf">PDF</button>
<button class="tab" data-tab="url">URL</button>
<button class="tab" data-tab="cloud">Cloud</button>
<button class="tab" data-tab="saved">Saved</button>
</div>
<!-- Text -->
<div class="tab-content active" id="tab-text">
<textarea id="text-input" placeholder="Paste your text here…"></textarea>
<button class="btn-primary" id="btn-load-text">Load Text</button>
</div>
<!-- PDF -->
<div class="tab-content" id="tab-pdf">
<div class="upload-area" id="upload-area">
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
<p>Drop PDF here or<br/><strong>click to upload</strong></p>
<small>Text is extracted from all pages so the AI can read it aloud</small>
<input type="file" id="pdf-input" accept=".pdf" hidden/>
</div>
<div id="pdf-status" class="status-text"></div>
</div>
<!-- URL -->
<div class="tab-content" id="tab-url">
<input type="url" id="url-input" class="text-field" placeholder="https://example.com/article"/>
<button class="btn-primary" id="btn-load-url">Fetch Article</button>
<div id="url-status" class="status-text"></div>
</div>
<!-- Nextcloud -->
<div class="tab-content" id="tab-cloud">
<div id="nc-path-bar" class="nc-path-bar">
<span id="nc-path-display">/</span>
<button class="btn-sm" id="nc-up-btn" onclick="ncUp()">↑ Up</button>
</div>
<div id="nc-file-list" class="nc-file-list">
<div class="status-text">Click Browse to load files</div>
</div>
<button class="btn-primary" id="btn-nc-browse" onclick="ncBrowse()">Browse Nextcloud</button>
<div id="nc-status" class="status-text"></div>
</div>
<!-- Saved Docs -->
<div class="tab-content" id="tab-saved">
<div id="saved-list" class="saved-list">
<div class="status-text">No saved documents</div>
</div>
<button class="btn-secondary" id="btn-refresh-saved" onclick="loadSavedDocs()">Refresh</button>
</div>
</aside>
<!-- Main Content -->
<main class="content-area" id="content-area">
<div id="document-header" class="document-header" style="display:none">
<div class="doc-title-row">
<h1 class="document-title" id="document-title"></h1>
<div class="doc-title-actions">
<!-- View toggle (only shown for PDFs) -->
<div class="view-toggle" id="view-toggle" style="display:none">
<button class="view-btn active" id="btn-view-text" onclick="setView('text')">Text</button>
<button class="view-btn" id="btn-view-pdf" onclick="setView('pdf')">PDF</button>
</div>
<button class="btn-save" id="btn-save-doc" title="Save document">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Save
</button>
</div>
</div>
<p class="document-meta" id="document-meta"></p>
<div class="doc-hint" id="doc-hint">Click any sentence to jump there · Space to play/pause · ← → to skip</div>
</div>
<!-- Text view -->
<div class="text-viewer" id="text-viewer">
<div class="empty-state">
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
<p>Load a document to get started</p>
<span>Paste text, upload a PDF, enter a URL, or pick from Nextcloud</span>
</div>
</div>
<!-- PDF view (PDF.js rendered pages) -->
<div class="pdf-viewer" id="pdf-viewer" style="display:none">
<div id="pdf-pages"></div>
</div>
</main>
<!-- Right Sidebar: Settings -->
<aside class="sidebar-right" id="sidebar-right">
<div class="sidebar-header"><h3>Voice Settings</h3></div>
<div class="setting-group">
<label class="setting-label">TTS Model</label>
<select id="model-select" class="select"></select>
<div class="lock-notice" id="lock-notice">Stop playback to change voice</div>
</div>
<div class="setting-group">
<label class="setting-label">Voice</label>
<select id="voice-select" class="select"></select>
</div>
<div class="setting-group">
<label class="setting-label">Speed — <span id="speed-label">1×</span></label>
<input type="range" id="speed-slider" min="0.5" max="3" step="0.25" value="1" class="slider"/>
<div class="speed-presets">
<button class="speed-btn" data-speed="0.75">0.75×</button>
<button class="speed-btn active" data-speed="1">1×</button>
<button class="speed-btn" data-speed="1.25">1.25×</button>
<button class="speed-btn" data-speed="1.5">1.5×</button>
<button class="speed-btn" data-speed="2">2×</button>
<button class="speed-btn" data-speed="3">3×</button>
</div>
</div>
<div class="setting-group">
<label class="setting-label">Font Size</label>
<div class="font-size-row">
<button class="btn-sz" id="font-decrease">A</button>
<span id="font-size-label">18px</span>
<button class="btn-sz" id="font-increase">A+</button>
</div>
</div>
<div class="setting-group">
<button class="btn-secondary" id="btn-preview">▶ Preview Voice</button>
</div>
</aside>
</div>
<!-- Player Bar -->
<div class="player-bar">
<div class="player-left">
<p class="now-playing" id="now-playing">No document loaded</p>
</div>
<div class="player-center">
<button class="ctrl-btn" id="btn-prev" title="Previous (←)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg>
</button>
<button class="play-btn" id="btn-play" title="Play/Pause (Space)">
<svg class="icon-play" width="26" height="26" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
<svg class="icon-pause" width="26" height="26" viewBox="0 0 24 24" fill="currentColor" style="display:none"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
</button>
<button class="ctrl-btn" id="btn-next" title="Next (→)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zm2.5-6 5.5 4V8l-5.5 4zM16 6h2v12h-2z"/></svg>
</button>
</div>
<div class="player-right">
<span class="progress-text"><span id="cur-sent">0</span> / <span id="tot-sent">0</span></span>
<div class="progress-track" id="progress-track">
<div class="progress-fill" id="progress-fill"></div>
</div>
</div>
</div>
</div>
<!-- Loading overlay -->
<div class="loading-overlay" id="loading-overlay">
<div class="spinner"></div>
<p id="loading-text">Loading…</p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="/static/app.js"></script>
</body>
</html>