Spaces:
Sleeping
Sleeping
| <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> |