|
<!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> |
|
|