NotebookMg / templates /index.html
TheM1N9
first commit
e56e019
raw
history blame
16.6 kB
<!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">
<h2>Login</h2>
<form action="/login" method="POST" class="login-form">
<div class="input-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required />
</div>
<div class="input-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required />
</div>
<button type="submit">Login</button>
</form>
</div>
</div>
{% 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();
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;
});
const formData = new FormData();
try {
formData.append("file", pdfFile.files[0]);
formData.append(
"tharun_voice_id",
document.getElementById("tharunVoiceId")?.value || ""
);
formData.append(
"akshara_voice_id",
document.getElementById("aksharaVoiceId")?.value || ""
);
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";
}
}
}
} catch (error) {
console.error("Error:", error);
if (error) {
error.textContent =
error.message || "An error occurred during upload";
error.style.display = "block";
}
} finally {
// Reset button state
if (submitButton) {
submitButton.disabled = false;
submitButton.innerHTML = "Generate Podcast";
}
// Re-enable inputs
inputs.forEach((input) => {
if (input) input.disabled = false;
});
}
};
async function regenerateSegment(index, newText = null) {
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("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>