File size: 8,741 Bytes
37f3c9b
 
 
 
 
d049735
37f3c9b
 
 
 
d049735
 
37f3c9b
 
 
d049735
37f3c9b
fa500c1
37f3c9b
5499b11
37f3c9b
5499b11
37f3c9b
 
5499b11
37f3c9b
 
 
 
 
 
 
 
21bddc4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37f3c9b
 
 
 
 
d049735
 
37f3c9b
 
 
 
 
21bddc4
37f3c9b
 
 
 
 
21bddc4
b33f725
37f3c9b
b33f725
37f3c9b
 
 
 
 
 
 
 
 
b33f725
fa500c1
37f3c9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21bddc4
 
 
37f3c9b
 
 
 
b33f725
37f3c9b
b33f725
37f3c9b
 
 
 
 
 
21bddc4
 
 
 
 
37f3c9b
 
 
 
 
 
 
 
 
 
 
b33f725
37f3c9b
b33f725
 
 
 
 
 
 
 
 
 
2ea6c41
37f3c9b
b33f725
 
 
 
 
37f3c9b
 
 
 
 
 
 
b33f725
 
21bddc4
37f3c9b
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<!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>