NoLev's picture
Update static/index.html
5499b11 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Podcast to Dark Romance Novel Converter</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen">
<div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-4xl">
<h1 class="text-2xl font-bold mb-4 text-center">Podcast to Dark Romance Novel Converter</h1>
<p class="text-center text-gray-600 mb-4">Generate explicit dark romance structures (open marriages, BDSM, swinging) from transcripts.</p>
<!-- Model Selection -->
<div class="mb-4">
<label for="model" class="block text-sm font-medium text-gray-700">Select AI Model (Explicit Content)</label>
<select id="model" class="mt-1 block w-full p-2 border border-gray-300 rounded-md" required>
<option value="nousresearch/hermes-3-llama-3.1-70b">Hermes 3 Llama 3.1 70B (Uncensored, Explicit)</option>
<option value="deepseek/deepseek-chat-v3-0324:free">DeepSeek Chat (Free)</option>
<option value="deepseek/deepseek-chat-v3-0324">DeepSeek Chat (Paid)</option>
<option value="deepseek/deepseek-r1-0528:free">DeepSeek R1 (Free)</option>
<option value="deepseek/deepseek-r1-0528">DeepSeek R1 (Paid)</option>
<option value="tngtech/deepseek-r1t2-chimera:free">DeepSeek TNG (Free)</option>
<option value="x-ai/grok-4">Grok 4</option>
<option value="x-ai/grok-4-fast">Grok 4</option>
<option value="x-ai/grok-3">Grok 3</option>
<option value="anthropic/claude-3.7-sonnet">Claude Sonnet 3.7</option>
<option value="anthropic/claude-3.5-sonnet">Claude 3.5 Sonnet</option>
<option value="openai/gpt-4o-mini">GPT-4o Mini</option>
<option value="meta-ai/llama-3.1-8b-instruct">LLaMA 3.1 8B Instruct</option>
</select>
</div>
<!-- Structure Config -->
<div class="mb-4 grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="chapter_count" class="block text-sm font-medium text-gray-700">Chapter Count</label>
<input type="number" id="chapter_count" value="10" min="1" max="50" class="mt-1 block w-full p-2 border border-gray-300 rounded-md">
</div>
<div>
<label for="word_count" class="block text-sm font-medium text-gray-700">Total Word Count</label>
<input type="number" id="word_count" value="90000" min="1000" max="500000" class="mt-1 block w-full p-2 border border-gray-300 rounded-md">
</div>
<div>
<label for="custom_prompt" class="block text-sm font-medium text-gray-700">Custom Prompt (Optional)</label>
<textarea id="custom_prompt" class="mt-1 block w-full p-2 border border-gray-300 rounded-md" rows="2" placeholder="e.g., Make the outline more gothic horror themed with vampire elements..."></textarea>
</div>
</div>
<!-- File Upload or Transcript Input -->
<div class="mb-4">
<label for="file" class="block text-sm font-medium text-gray-700">Upload Transcript File (TXT or PDF)</label>
<input type="file" id="file" accept=".txt,.pdf" class="mt-1 block w-full p-2 border border-gray-300 rounded-md">
<p class="text-sm text-gray-500 mt-1">Or paste below:</p>
<label for="transcript" class="block text-sm font-medium text-gray-700 mt-2">Podcast Transcript (Explicit Content OK)</label>
<textarea id="transcript" class="mt-1 block w-full p-2 border border-gray-300 rounded-md" rows="10" placeholder="Paste your podcast transcript here... (Themes: open marriages, BDSM, swinging)"></textarea>
<p id="transcript_count" class="text-sm text-gray-500 mt-1">Characters: 0</p>
</div>
<!-- Generate Button -->
<div class="mb-4">
<button id="generate_novel" class="w-full bg-purple-600 text-white p-2 rounded-md hover:bg-purple-700">Generate Novel Structure</button>
</div>
<!-- Outputs -->
<div id="outputs" class="space-y-4 hidden">
<div>
<label class="block text-sm font-medium text-gray-700">Outline</label>
<textarea id="outline" class="mt-1 block w-full p-2 border border-gray-300 rounded-md" rows="20" readonly placeholder="Streaming generation..."></textarea>
</div>
<button id="copy_all" class="mt-2 bg-green-600 text-white p-2 rounded-md hover:bg-green-700">Copy Outline for NovelCrafter</button>
</div>
</div>
<script>
const transcriptTextarea = document.getElementById("transcript");
const fileInput = document.getElementById("file");
const countDisplay = document.getElementById("transcript_count");
const generateButton = document.getElementById("generate_novel");
const outputsDiv = document.getElementById("outputs");
const outlineTextarea = document.getElementById("outline");
// Handle file upload: read and populate textarea
fileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
transcriptTextarea.value = e.target.result;
updateCount();
};
reader.onerror = () => {
alert("Error reading file. Please paste manually.");
};
if (file.type === 'application/pdf') {
alert("PDF files will be processed on the server. Paste or upload will work, but preview not available in textarea.");
transcriptTextarea.value = "[PDF content will be extracted on server]";
} else {
reader.readAsText(file);
}
updateCount();
}
});
function updateCount() {
const charCount = transcriptTextarea.value.length;
countDisplay.textContent = `Characters: ${charCount}`;
}
transcriptTextarea.addEventListener("input", updateCount);
generateButton.addEventListener("click", async () => {
const model = document.getElementById("model").value;
const transcript = transcriptTextarea.value.trim();
const file = fileInput.files[0];
const chapterCount = document.getElementById("chapter_count").value;
const wordCount = document.getElementById("word_count").value;
const customPrompt = document.getElementById("custom_prompt").value.trim();
if (!transcript && !file) {
alert("Please enter a transcript or select a file.");
return;
}
outlineTextarea.value = ""; // Clear previous
const statusDiv = document.createElement("div");
statusDiv.textContent = "Streaming: Summarizing transcript & generating outline...";
statusDiv.className = "text-center text-blue-600 mb-4";
outputsDiv.insertBefore(statusDiv, outputsDiv.firstChild);
outputsDiv.classList.remove("hidden");
try {
const formData = new FormData();
formData.append('model', model);
formData.append('chapter_count', chapterCount);
formData.append('word_count', wordCount);
if (customPrompt) {
formData.append('custom_prompt', customPrompt);
}
if (file) {
formData.append('file', file);
} else {
formData.append('transcript', transcript);
}
const response = await fetch("/generate_novel", {
method: "POST",
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
const { value, done: readerDone } = await reader.read();
done = readerDone;
if (value) {
const chunk = decoder.decode(value, { stream: true });
outlineTextarea.value += chunk; // Append in real-time
outlineTextarea.scrollTop = outlineTextarea.scrollHeight; // Auto-scroll
}
}
statusDiv.remove();
if (outlineTextarea.value.includes("Error:")) {
statusDiv.textContent = outlineTextarea.value;
statusDiv.className = "text-center text-red-600 mb-4";
}
} catch (error) {
statusDiv.textContent = `Error: ${error.message}`;
statusDiv.className = "text-center text-red-600 mb-4";
}
});
document.getElementById("copy_all").addEventListener("click", () => {
const outline = document.getElementById("outline").value;
navigator.clipboard.writeText(outline)
.then(() => alert("Outline copied to clipboard! Paste into NovelCrafter for full manuscript."))
.catch(err => console.error('Failed to copy: ', err));
});
</script>
</body>
</html>