| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>NotebookMg - PDF to Podcast Converter</title> |
| <link |
| rel="stylesheet" |
| href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" |
| /> |
| <link rel="stylesheet" href="/static/styles.css" /> |
| </head> |
| <body> |
| {% if not is_authenticated %} |
| <div class="login-container"> |
| <div class="login-card"> |
| <div class="auth-tabs"> |
| <button class="tab-btn active" onclick="switchTab('login')"> |
| <i class="fas fa-sign-in-alt"></i> Login |
| </button> |
| <button class="tab-btn" onclick="switchTab('signup')"> |
| <i class="fas fa-user-plus"></i> Sign Up |
| </button> |
| </div> |
|
|
| <div id="login-content" class="tab-content active"> |
| <h2><i class="fas fa-lock"></i> Login</h2> |
| <form action="/login" method="POST" class="login-form"> |
| <div class="input-group"> |
| <label for="username"><i class="fas fa-user"></i> Username</label> |
| <input |
| type="text" |
| id="username" |
| name="username" |
| placeholder="Enter your username" |
| required |
| /> |
| </div> |
| <div class="input-group"> |
| <label for="password"><i class="fas fa-key"></i> Password</label> |
| <input |
| type="password" |
| id="password" |
| name="password" |
| placeholder="Enter your password" |
| required |
| /> |
| </div> |
| <button type="submit"> |
| <i class="fas fa-sign-in-alt"></i> Login |
| </button> |
| </form> |
| </div> |
|
|
| <div id="signup-content" class="tab-content"> |
| <h2><i class="fas fa-user-plus"></i> Sign Up</h2> |
| <div class="signup-section"> |
| <i class="fas fa-envelope-open-text fa-3x"></i> |
| <p> |
| Due to skyrocketing API costs, access to NotebookMg is only for |
| the chosen ones. |
| </p> |
| <p class="contact-text"> |
| To see if you're worthy of entry, hit up |
| <span>Ronit</span> |
| or |
| <span>Manikanta K</span> |
| on Slack—they're the gatekeepers 🔮 |
| </p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| function switchTab(tab) { |
| |
| document.querySelectorAll(".tab-btn").forEach((btn) => { |
| btn.classList.remove("active"); |
| }); |
| event.currentTarget.classList.add("active"); |
| |
| |
| document.querySelectorAll(".tab-content").forEach((content) => { |
| content.classList.remove("active"); |
| }); |
| document.getElementById(`${tab}-content`).classList.add("active"); |
| } |
| </script> |
| {% else %} |
| <div class="hero"> |
| <h1>NotebookMg</h1> |
| <p>Transform your PDFs into engaging podcasts with AI-powered voices</p> |
| </div> |
|
|
| <div class="container"> |
| <div class="card"> |
| <div class="features"> |
| <div class="feature"> |
| <i class="fas fa-file-pdf"></i> |
| <h3>PDF Processing</h3> |
| <p>Smart text extraction and cleaning</p> |
| </div> |
| <div class="feature"> |
| <i class="fas fa-microphone-alt"></i> |
| <h3>Natural Voices</h3> |
| <p>Realistic AI-powered conversations</p> |
| </div> |
| <div class="feature"> |
| <i class="fas fa-podcast"></i> |
| <h3>Podcast Generation</h3> |
| <p>Engaging audio content creation</p> |
| </div> |
| </div> |
|
|
| <form id="uploadForm"> |
| <div class="upload-section" id="dropZone"> |
| <i |
| class="fas fa-cloud-upload-alt fa-3x" |
| style="color: #4caf50; margin-bottom: 15px" |
| ></i> |
| <h3>Upload your PDF</h3> |
| <p>Drag and drop your file here or click to browse</p> |
| <input |
| type="file" |
| id="pdfFile" |
| accept=".pdf" |
| required |
| style="display: none" |
| /> |
| <button |
| type="button" |
| onclick="document.getElementById('pdfFile').click()" |
| > |
| Choose File |
| </button> |
| <p id="selectedFile" style="margin-top: 10px; color: #888"></p> |
| </div> |
|
|
| <div class="voice-inputs"> |
| <div class="input-group"> |
| <label for="tharunVoiceId">Tharun Voice ID</label> |
| <input |
| type="text" |
| id="tharunVoiceId" |
| placeholder="Enter Tharun voice ID" |
| required |
| /> |
| </div> |
| <div class="input-group"> |
| <label for="aksharaVoiceId">Akshara Voice ID</label> |
| <input |
| type="text" |
| id="aksharaVoiceId" |
| placeholder="Enter Akshara voice ID" |
| required |
| /> |
| </div> |
| </div> |
|
|
| <button type="submit">Generate Podcast</button> |
| </form> |
|
|
| <div |
| id="error" |
| class="error" |
| style="color: #ff4444; margin: 10px 0; display: none" |
| ></div> |
|
|
| <div id="audio-result" class="audio-container"> |
| <div class="audio-header"> |
| <h3>Your Podcast</h3> |
| <audio id="podcast-player" controls> |
| Your browser does not support the audio element. |
| </audio> |
| </div> |
| </div> |
|
|
| <details |
| id="segments-container" |
| class="segments-container" |
| style="display: none" |
| > |
| <summary class="segments-summary"> |
| <i class="fas fa-chevron-right"></i> |
| Individual Segments |
| <span class="segment-count"></span> |
| </summary> |
| <div id="segments-list"></div> |
| </details> |
| </div> |
| </div> |
|
|
| <script> |
| document.getElementById("uploadForm").onsubmit = async (e) => { |
| e.preventDefault(); |
| |
| // Get authentication status from URL |
| const urlParams = new URLSearchParams(window.location.search); |
| const isAuthenticated = urlParams.get("authenticated") === "true"; |
| |
| if (!isAuthenticated) { |
| alert("Please log in first"); |
| return; |
| } |
| |
| // Add authentication to FormData |
| const formData = new FormData(); |
| formData.append("authenticated", "true"); |
| formData.append("file", document.getElementById("pdfFile").files[0]); |
| formData.append( |
| "tharun_voice_id", |
| document.getElementById("tharunVoiceId").value |
| ); |
| formData.append( |
| "akshara_voice_id", |
| document.getElementById("aksharaVoiceId").value |
| ); |
| |
| console.log("Form submission started"); |
| |
| const submitButton = e.target.querySelector("button[type='submit']"); |
| const audioResult = document.getElementById("audio-result"); |
| const segmentsContainer = document.getElementById("segments-container"); |
| const error = document.getElementById("error"); |
| const inputs = e.target.querySelectorAll("input"); |
| const pdfFile = document.getElementById("pdfFile"); |
| |
| // Check if file is selected |
| if (!pdfFile || !pdfFile.files || pdfFile.files.length === 0) { |
| if (error) { |
| error.textContent = "Please select a PDF file"; |
| error.style.display = "block"; |
| } |
| return; |
| } |
| |
| // Clear previous results if elements exist |
| if (audioResult) audioResult.style.display = "none"; |
| if (segmentsContainer) segmentsContainer.style.display = "none"; |
| if (document.getElementById("segments-list")) { |
| document.getElementById("segments-list").innerHTML = ""; |
| } |
| if (document.getElementById("podcast-player")) { |
| document.getElementById("podcast-player").src = ""; |
| } |
| if (error) error.style.display = "none"; |
| |
| // Update button state |
| if (submitButton) { |
| submitButton.disabled = true; |
| submitButton.innerHTML = |
| '<i class="fas fa-spinner fa-spin"></i> Generating Podcast... May take few minutes...'; |
| } |
| |
| // Disable inputs |
| inputs.forEach((input) => { |
| if (input) input.disabled = true; |
| }); |
| |
| console.log("Sending request to server..."); |
| const response = await fetch("/upload-pdf/", { |
| method: "POST", |
| body: formData, |
| }); |
| |
| console.log("Server response received:", response.status); |
| |
| if (!response.ok) { |
| throw new Error(`HTTP error! status: ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| console.log("Response data:", data); |
| |
| if (audioResult && data.podcast_file) { |
| const audioPlayer = document.getElementById("podcast-player"); |
| if (audioPlayer) { |
| audioPlayer.src = `/download/${data.podcast_file}`; |
| audioResult.style.display = "block"; |
| } |
| |
| const segmentsList = document.getElementById("segments-list"); |
| if (segmentsList && data.segments) { |
| segmentsList.innerHTML = ""; |
| |
| const segmentCount = document.querySelector(".segment-count"); |
| if (segmentCount) { |
| segmentCount.textContent = `(${data.segments.length} segments)`; |
| } |
| |
| data.segments.forEach((segment, index) => { |
| const segmentDiv = document.createElement("div"); |
| segmentDiv.className = "segment"; |
| segmentDiv.id = `segment-${index}`; |
| segmentDiv.innerHTML = ` |
| <div class="segment-info"> |
| <div class="segment-header"> |
| <div class="segment-speaker">${segment.speaker}</div> |
| <button class="edit-btn" onclick="makeEditable(${index})"> |
| <i class="fas fa-edit"></i> Edit |
| </button> |
| </div> |
| <div class="segment-text"> |
| <div class="segment-text-content">${segment.text}</div> |
| </div> |
| </div> |
| <div class="segment-controls"> |
| <audio controls src="/download/${segment.file}"></audio> |
| <button class="regenerate-btn" onclick="regenerateSegment(${index})"> |
| <i class="fas fa-redo"></i> Regenerate |
| </button> |
| </div> |
| `; |
| segmentsList.appendChild(segmentDiv); |
| }); |
| |
| if (segmentsContainer) { |
| segmentsContainer.style.display = "block"; |
| } |
| } |
| } |
| }; |
| |
| async function regenerateSegment(index, newText = null) { |
| // Get authentication status from URL |
| const urlParams = new URLSearchParams(window.location.search); |
| const isAuthenticated = urlParams.get("authenticated") === "true"; |
| |
| if (!isAuthenticated) { |
| alert("Please log in first"); |
| return; |
| } |
| |
| const segment = document.querySelector(`#segment-${index}`); |
| const speaker = segment.querySelector(".segment-speaker").textContent; |
| const text = |
| newText || segment.querySelector(".segment-text-content").textContent; |
| const audio = segment.querySelector("audio"); |
| const button = segment.querySelector(".regenerate-btn"); |
| const mainPodcastPlayer = document.getElementById("podcast-player"); |
| const currentMainTime = mainPodcastPlayer |
| ? mainPodcastPlayer.currentTime |
| : 0; |
| |
| // Disable the button and show loading state |
| button.disabled = true; |
| button.innerHTML = |
| '<i class="fas fa-spinner fa-spin"></i> Regenerating...'; |
| |
| try { |
| const formData = new FormData(); |
| formData.append("authenticated", "true"); |
| formData.append("speaker", speaker); |
| formData.append("text", text); |
| formData.append( |
| "tharun_voice_id", |
| document.getElementById("tharunVoiceId").value |
| ); |
| formData.append( |
| "akshara_voice_id", |
| document.getElementById("aksharaVoiceId").value |
| ); |
| |
| const response = await fetch(`/regenerate-segment/${index}`, { |
| method: "POST", |
| body: formData, |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`HTTP error! status: ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| |
| if (data.success) { |
| // Update the segment audio |
| if (audio) { |
| const newSegmentSrc = `/download/${data.segment_file}`; |
| audio.src = newSegmentSrc; |
| await audio.load(); // Wait for the audio to load |
| } |
| |
| // Update the main podcast player |
| if (mainPodcastPlayer && data.podcast_file) { |
| const newPodcastSrc = `/download/${data.podcast_file}`; |
| mainPodcastPlayer.src = newPodcastSrc; |
| await mainPodcastPlayer.load(); // Wait for the audio to load |
| |
| // Try to restore the previous playback position |
| try { |
| mainPodcastPlayer.currentTime = currentMainTime; |
| } catch (e) { |
| console.warn("Couldn't restore playback position:", e); |
| } |
| } |
| |
| // Show success state briefly |
| button.innerHTML = '<i class="fas fa-check"></i> Success!'; |
| setTimeout(() => { |
| button.innerHTML = '<i class="fas fa-redo"></i> Regenerate'; |
| button.disabled = false; |
| }, 2000); |
| } else { |
| throw new Error(data.detail || "Regeneration failed"); |
| } |
| } catch (error) { |
| console.error("Error:", error); |
| button.innerHTML = |
| '<i class="fas fa-exclamation-triangle"></i> Failed'; |
| setTimeout(() => { |
| button.innerHTML = '<i class="fas fa-redo"></i> Regenerate'; |
| button.disabled = false; |
| }, 2000); |
| } |
| } |
| |
| // Add drag and drop functionality |
| const dropZone = document.getElementById("dropZone"); |
| const pdfFile = document.getElementById("pdfFile"); |
| const selectedFile = document.getElementById("selectedFile"); |
| |
| ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => { |
| dropZone.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| ["dragenter", "dragover"].forEach((eventName) => { |
| dropZone.addEventListener(eventName, highlight, false); |
| }); |
| |
| ["dragleave", "drop"].forEach((eventName) => { |
| dropZone.addEventListener(eventName, unhighlight, false); |
| }); |
| |
| function highlight(e) { |
| dropZone.classList.add("highlight"); |
| } |
| |
| function unhighlight(e) { |
| dropZone.classList.remove("highlight"); |
| } |
| |
| dropZone.addEventListener("drop", handleDrop, false); |
| |
| function handleDrop(e) { |
| const dt = e.dataTransfer; |
| const files = dt.files; |
| pdfFile.files = files; |
| updateFileName(); |
| } |
| |
| pdfFile.addEventListener("change", updateFileName); |
| |
| function updateFileName() { |
| if (pdfFile.files.length > 0) { |
| selectedFile.textContent = `Selected file: ${pdfFile.files[0].name}`; |
| } |
| } |
| |
| function makeEditable(index) { |
| const segment = document.querySelector(`#segment-${index}`); |
| const textDiv = segment.querySelector(".segment-text"); |
| const originalText = textDiv.querySelector( |
| ".segment-text-content" |
| ).textContent; |
| const editButton = segment.querySelector(".edit-btn"); |
| |
| // Hide the edit button |
| editButton.style.display = "none"; |
| |
| // Add textarea and controls |
| textDiv.innerHTML = ` |
| <textarea>${originalText}</textarea> |
| <div class="edit-controls" style="justify-content: flex-end;"> |
| <button class="save-btn" onclick="saveEdit(${index})"> |
| <i class="fas fa-save"></i> Save |
| </button> |
| <button class="cancel-btn" onclick="cancelEdit(${index}, '${originalText.replace( |
| /'/g, |
| "\\'" |
| )}')"> |
| <i class="fas fa-times"></i> Cancel |
| </button> |
| </div> |
| `; |
| } |
| |
| function cancelEdit(index, originalText) { |
| const segment = document.querySelector(`#segment-${index}`); |
| const textDiv = segment.querySelector(".segment-text"); |
| const editButton = segment.querySelector(".edit-btn"); |
| |
| // Show the edit button again |
| editButton.style.display = "flex"; |
| |
| // Restore original content |
| textDiv.innerHTML = ` |
| <div class="segment-text-content">${originalText}</div> |
| `; |
| } |
| |
| async function saveEdit(index) { |
| const segment = document.querySelector(`#segment-${index}`); |
| const textarea = segment.querySelector("textarea"); |
| const newText = textarea.value; |
| const editButton = segment.querySelector(".edit-btn"); |
| |
| // Show loading state in save button |
| const saveBtn = segment.querySelector(".save-btn"); |
| saveBtn.disabled = true; |
| saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...'; |
| |
| try { |
| await regenerateSegment(index, newText); |
| |
| // Show the edit button again |
| editButton.style.display = "flex"; |
| |
| // Update the text display |
| const textDiv = segment.querySelector(".segment-text"); |
| textDiv.innerHTML = `<div class="segment-text-content">${newText}</div>`; |
| } catch (error) { |
| console.error("Error saving edit:", error); |
| alert("Failed to save changes. Please try again."); |
| |
| // Reset save button |
| saveBtn.disabled = false; |
| saveBtn.innerHTML = '<i class="fas fa-save"></i> Save'; |
| } |
| } |
| </script> |
| {% endif %} |
| </body> |
| </html> |
|
|