eubottura commited on
Commit
85774d3
·
verified ·
1 Parent(s): 5921f34

Cara, e o imput do audio que vai ler com whipser, transcript e ainda vai gerar o arquivo srt pro capcuty, com o tempo exato perfeito, de cada bloco

Browse files
Files changed (3) hide show
  1. index.html +51 -4
  2. script.js +214 -55
  3. style.css +16 -2
index.html CHANGED
@@ -88,17 +88,61 @@
88
 
89
  <!-- Main Content -->
90
  <main class="flex-grow container mx-auto px-4 py-8">
91
- <header class="mb-10 text-center">
92
  <h1 class="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary-400 to-secondary-400 mb-4">
93
  Script Alignment Specialist
94
  </h1>
95
  <p class="text-slate-400 max-w-2xl mx-auto text-lg">
96
- Transform your raw scripts into "breathable" SRT blocks optimized for CapCut.
97
  Zero-error line breaks, strict character limits, and natural rhythm.
98
  </p>
99
  </header>
100
 
101
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 h-full">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  <!-- Input Section -->
104
  <section class="flex flex-col gap-4">
@@ -148,9 +192,12 @@
148
  <button id="copy-btn" class="text-xs bg-slate-800 hover:bg-slate-700 text-white px-3 py-1 rounded flex items-center gap-1 transition-colors">
149
  <i data-feather="copy" class="w-3 h-3"></i> Copy
150
  </button>
 
 
 
151
  </div>
152
  </div>
153
- <div class="glass-panel rounded-b-xl p-1 flex-grow relative">
154
  <textarea id="output-text" readonly
155
  class="w-full h-96 lg:h-[500px] bg-slate-900/50 text-primary-200 p-4 rounded-lg resize-none focus:outline-none mono-font text-sm leading-relaxed"
156
  placeholder="Processed blocks will appear here..."></textarea>
 
88
 
89
  <!-- Main Content -->
90
  <main class="flex-grow container mx-auto px-4 py-8">
91
+ <header class="mb-8 text-center">
92
  <h1 class="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary-400 to-secondary-400 mb-4">
93
  Script Alignment Specialist
94
  </h1>
95
  <p class="text-slate-400 max-w-2xl mx-auto text-lg">
96
+ Transform your raw scripts or audio files into "breathable" SRT blocks optimized for CapCut.
97
  Zero-error line breaks, strict character limits, and natural rhythm.
98
  </p>
99
  </header>
100
 
101
+ <!-- Mode Toggle -->
102
+ <div class="flex justify-center mb-8">
103
+ <div class="glass-panel p-1 rounded-xl inline-flex">
104
+ <button id="mode-text" class="px-6 py-2 rounded-lg text-sm font-semibold transition-all bg-slate-700 text-white shadow-md">
105
+ Text Input
106
+ </button>
107
+ <button id="mode-audio" class="px-6 py-2 rounded-lg text-sm font-semibold transition-all text-slate-400 hover:text-white">
108
+ Audio Transcribe
109
+ </button>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- Audio Input Section (Hidden by default) -->
114
+ <section id="audio-section" class="hidden mb-8 glass-panel rounded-2xl p-6">
115
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
116
+ <div>
117
+ <label class="block text-sm font-medium text-slate-300 mb-2">OpenAI API Key</label>
118
+ <input type="password" id="api-key" placeholder="sk-..."
119
+ class="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-primary-500 transition-colors">
120
+ <p class="text-xs text-slate-500 mt-2">Required for Whisper transcription. Your key is used locally and not stored.</p>
121
+ </div>
122
+ <div>
123
+ <label class="block text-sm font-medium text-slate-300 mb-2">Upload Audio/Video</label>
124
+ <div class="relative border-2 border-dashed border-slate-700 rounded-lg p-4 hover:border-primary-500 transition-colors cursor-pointer bg-slate-900/50">
125
+ <input type="file" id="audio-file" accept="audio/*,video/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
126
+ <div class="text-center">
127
+ <i data-feather="mic" class="w-8 h-8 mx-auto text-slate-500 mb-2"></i>
128
+ <p class="text-sm text-slate-400" id="file-label">Click or drag audio file here</p>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ <div class="mt-6 flex justify-end">
134
+ <button id="transcribe-btn" class="bg-primary-600 hover:bg-primary-500 text-white px-8 py-3 rounded-xl font-semibold shadow-lg flex items-center gap-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
135
+ <i data-feather="zap" class="w-5 h-5"></i> Transcribe & Process
136
+ </button>
137
+ </div>
138
+ <div id="transcribing-status" class="hidden mt-4">
139
+ <div class="flex items-center gap-3 text-primary-400">
140
+ <i data-feather="loader" class="animate-spin w-5 h-5"></i>
141
+ <span class="text-sm">Transcribing audio... this may take a moment.</span>
142
+ </div>
143
+ </div>
144
+ </section>
145
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 h-full">
146
 
147
  <!-- Input Section -->
148
  <section class="flex flex-col gap-4">
 
192
  <button id="copy-btn" class="text-xs bg-slate-800 hover:bg-slate-700 text-white px-3 py-1 rounded flex items-center gap-1 transition-colors">
193
  <i data-feather="copy" class="w-3 h-3"></i> Copy
194
  </button>
195
+ <button id="download-srt-btn" class="hidden text-xs bg-primary-600 hover:bg-primary-500 text-white px-3 py-1 rounded flex items-center gap-1 transition-colors">
196
+ <i data-feather="download" class="w-3 h-3"></i> .SRT
197
+ </button>
198
  </div>
199
  </div>
200
+ <div class="glass-panel rounded-b-xl p-1 flex-grow relative">
201
  <textarea id="output-text" readonly
202
  class="w-full h-96 lg:h-[500px] bg-slate-900/50 text-primary-200 p-4 rounded-lg resize-none focus:outline-none mono-font text-sm leading-relaxed"
203
  placeholder="Processed blocks will appear here..."></textarea>
script.js CHANGED
@@ -1,4 +1,6 @@
 
1
  document.addEventListener('DOMContentLoaded', () => {
 
2
  const inputText = document.getElementById('input-text');
3
  const outputText = document.getElementById('output-text');
4
  const processBtn = document.getElementById('process-btn');
@@ -6,8 +8,25 @@ document.addEventListener('DOMContentLoaded', () => {
6
  const copyBtn = document.getElementById('copy-btn');
7
  const clearBtn = document.getElementById('clear-btn');
8
  const blockCount = document.getElementById('block-count');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- // --- CONSTANTS ---
 
 
 
11
  const MAX_CHARS = 11;
12
 
13
  // Tabu words (Articles, Prepositions, Pronouns, Conjunctions)
@@ -23,11 +42,187 @@ document.addEventListener('DOMContentLoaded', () => {
23
  ]);
24
 
25
  const CONNECTIVES = new Set(['e', 'é', 'que', 'and', 'that', 'y']); // Must start new line
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  // --- UTILITIES ---
28
 
29
  function showToast(message, type = 'success') {
30
- const toast = document.createElement('div');
31
  const bgColor = type === 'success' ? 'bg-green-500' : 'bg-red-500';
32
  toast.className = `fixed bottom-5 right-5 ${bgColor} text-white px-6 py-3 rounded-lg shadow-lg flex items-center gap-2 z-50 toast`;
33
  toast.innerHTML = `<i data-feather="${type === 'success' ? 'check-circle' : 'alert-circle'}"></i> ${message}`;
@@ -73,18 +268,9 @@ document.addEventListener('DOMContentLoaded', () => {
73
  return block.replace(/\s/g, '').length;
74
  }
75
 
76
- function processScript() {
77
- const raw = inputText.value;
78
- if (!raw.trim()) {
79
- showToast("Please enter text to process.", "error");
80
- return;
81
- }
82
-
83
  const cleanedText = cleanText(raw);
84
-
85
- // Step: Split into initial blocks based on rules
86
- // We will build lines dynamically.
87
-
88
  const words = cleanedText.split(' ');
89
  let lines = [];
90
  let currentLine = [];
@@ -94,29 +280,18 @@ document.addEventListener('DOMContentLoaded', () => {
94
  let word = words[i];
95
  let nextWord = words[i + 1] || '';
96
 
97
- // Rule: Split at Punctuation (!, ?, .)
98
- // If word ends with !, ?, or .. -> End line immediately
99
- const endsWithPunctuation = /[!?]|(\.\.)/.test(word.slice(-1)); // simplified check
100
-
101
- // Rule: Connectives (E/É/QUE/AND/THAT) -> Start new line
102
  const isConnective = CONNECTIVES.has(word.toLowerCase().replace(/[!?.,]/g, ''));
103
 
104
- // Logic: Should we start a new line?
105
  let startNewLine = false;
106
 
107
  if (isConnective && currentLine.length > 0) {
108
  startNewLine = true;
109
  }
110
 
111
- // Check length if we add this word to current line
112
- // Calculate length of current line + space + word (without spaces in final count logic)
113
- // But we need to build the string to check "No spaces" limit.
114
  const proposedLineStr = [...currentLine, word].join(' ');
115
  const proposedLen = countCharsNoSpaces(proposedLineStr);
116
 
117
- // Length Enforcement (only if not single word line)
118
- // Note: Single word exception allows > 11 chars.
119
- // If currentLine is empty, we can take the word regardless of length (mostly).
120
  if (currentLine.length > 0 && proposedLen > MAX_CHARS) {
121
  startNewLine = true;
122
  }
@@ -128,7 +303,6 @@ document.addEventListener('DOMContentLoaded', () => {
128
  currentLine.push(word);
129
  }
130
 
131
- // Punctuation Split (If word ends with punctuation, it forces a break *after* it)
132
  if (endsWithPunctuation) {
133
  lines.push(currentLine.join(' '));
134
  currentLine = [];
@@ -139,19 +313,8 @@ document.addEventListener('DOMContentLoaded', () => {
139
  lines.push(currentLine.join(' '));
140
  }
141
 
142
- // --- Step: Anti-Weakening Correction ---
143
- // We need to iterate and fix weak endings.
144
- // A block is weak if the last word is <= 3 chars or Tabu.
145
- // Correction: Move the last word to the beginning of the NEXT block.
146
-
147
- // This is a delicate loop. We might need multiple passes or a specific algorithm.
148
- // Let's try a pass from end to start or start to end.
149
- // Since moving a word forward increases the length of the NEXT line, we might break that next line's length rule.
150
- // The prompt says: "move the offending word to the beginning of the next block... even if it pushes the next block's length."
151
- // So length is secondary to the anti-weakening rule.
152
-
153
  let changed = true;
154
- // Safety break to prevent infinite loops
155
  let iterations = 0;
156
 
157
  while (changed && iterations < 100) {
@@ -163,30 +326,20 @@ document.addEventListener('DOMContentLoaded', () => {
163
  const lastWord = lineWords[lineWords.length - 1];
164
 
165
  if (isWeakEnding(lastWord)) {
166
- // Move lastWord to next line
167
  const remainingWords = lineWords.slice(0, lineWords.length - 1);
168
 
169
  if (remainingWords.length === 0) {
170
- // The line consists ONLY of this weak word.
171
- // It moves to the next line entirely.
172
- // This implies the previous line might now be weak?
173
- // But strictly, we just shift it down.
174
- lines[i] = lines[i+1]; // Pull next line up? No, that's messy.
175
- // Better: Merge this word into the start of next line.
176
- // If the current line is empty after removal, delete it.
177
  } else {
178
  lines[i] = remainingWords.join(' ');
179
  }
180
 
181
- // Add to next line
182
- // We construct the next line: [movedWord] + [oldNextLineWords]
183
  const nextLineWords = lines[i+1].split(' ');
184
  lines[i+1] = [lastWord, ...nextLineWords].join(' ');
185
 
186
- // If current line became empty, remove it
187
  if (lines[i].trim() === '') {
188
  lines.splice(i, 1);
189
- i--; // adjust index
190
  }
191
 
192
  changed = true;
@@ -194,12 +347,18 @@ document.addEventListener('DOMContentLoaded', () => {
194
  }
195
  }
196
 
197
- // Clean up any empty lines that might have been generated by edge cases
198
- lines = lines.filter(l => l.trim().length > 0);
 
 
 
 
 
 
 
199
 
200
- // Output
201
- outputText.value = lines.join('\n');
202
- blockCount.textContent = lines.length;
203
 
204
  showToast("Script processed successfully!");
205
  }
 
1
+
2
  document.addEventListener('DOMContentLoaded', () => {
3
+ // UI Elements
4
  const inputText = document.getElementById('input-text');
5
  const outputText = document.getElementById('output-text');
6
  const processBtn = document.getElementById('process-btn');
 
8
  const copyBtn = document.getElementById('copy-btn');
9
  const clearBtn = document.getElementById('clear-btn');
10
  const blockCount = document.getElementById('block-count');
11
+ const downloadSrtBtn = document.getElementById('download-srt-btn');
12
+
13
+ // Mode Toggle Elements
14
+ const modeTextBtn = document.getElementById('mode-text');
15
+ const modeAudioBtn = document.getElementById('mode-audio');
16
+ const audioSection = document.getElementById('audio-section');
17
+ const inputSection = document.querySelector('section'); // First section is input
18
+
19
+ // Audio Elements
20
+ const audioFileInput = document.getElementById('audio-file');
21
+ const apiKeyInput = document.getElementById('api-key');
22
+ const transcribeBtn = document.getElementById('transcribe-btn');
23
+ const fileLabel = document.getElementById('file-label');
24
+ const transcribingStatus = document.getElementById('transcribing-status');
25
 
26
+ // State
27
+ let currentSrtData = null;
28
+ let currentTranscriptWords = []; // Stores {word, start, end}
29
+ // --- CONSTANTS ---
30
  const MAX_CHARS = 11;
31
 
32
  // Tabu words (Articles, Prepositions, Pronouns, Conjunctions)
 
42
  ]);
43
 
44
  const CONNECTIVES = new Set(['e', 'é', 'que', 'and', 'that', 'y']); // Must start new line
45
+ // --- MODE SWITCHING ---
46
+ modeTextBtn.addEventListener('click', () => {
47
+ modeTextBtn.classList.add('mode-active');
48
+ modeAudioBtn.classList.remove('mode-active');
49
+ audioSection.classList.add('hidden');
50
+ inputSection.classList.remove('opacity-50', 'pointer-events-none');
51
+ downloadSrtBtn.classList.add('hidden');
52
+ outputText.value = '';
53
+ blockCount.textContent = '0';
54
+ currentSrtData = null;
55
+ });
56
+
57
+ modeAudioBtn.addEventListener('click', () => {
58
+ modeAudioBtn.classList.add('mode-active');
59
+ modeTextBtn.classList.remove('mode-active');
60
+ audioSection.classList.remove('hidden');
61
+ // Optional: Disable manual text input when in audio mode to avoid confusion
62
+ inputSection.classList.add('opacity-50', 'pointer-events-none');
63
+ });
64
+
65
+ audioFileInput.addEventListener('change', (e) => {
66
+ if (e.target.files.length > 0) {
67
+ fileLabel.textContent = e.target.files[0].name;
68
+ } else {
69
+ fileLabel.textContent = 'Click or drag audio file here';
70
+ }
71
+ });
72
+
73
+ // --- AUDIO TRANSCRIPTION LOGIC ---
74
+
75
+ async function handleTranscription() {
76
+ const file = audioFileInput.files[0];
77
+ const apiKey = apiKeyInput.value.trim();
78
+
79
+ if (!file) {
80
+ showToast("Please select an audio or video file.", "error");
81
+ return;
82
+ }
83
+ if (!apiKey) {
84
+ showToast("Please enter your OpenAI API Key.", "error");
85
+ return;
86
+ }
87
+
88
+ // UI Loading State
89
+ transcribeBtn.disabled = true;
90
+ transcribingStatus.classList.remove('hidden');
91
+
92
+ try {
93
+ const formData = new FormData();
94
+ formData.append('file', file);
95
+ formData.append('model', 'whisper-1');
96
+ formData.append('response_format', 'verbose_json');
97
+ formData.append('timestamp_granularities', 'word');
98
+
99
+ const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
100
+ method: 'POST',
101
+ headers: {
102
+ 'Authorization': `Bearer ${apiKey}`
103
+ },
104
+ body: formData
105
+ });
106
+
107
+ if (!response.ok) {
108
+ const errData = await response.json();
109
+ throw new Error(errData.error?.message || 'Transcription failed');
110
+ }
111
+
112
+ const data = await response.json();
113
+
114
+ // Extract words with timestamps
115
+ // Whisper returns words array when timestamp_granularities is set
116
+ if (!data.words) {
117
+ throw new Error("No word-level timestamps returned. Check API plan.");
118
+ }
119
+
120
+ currentTranscriptWords = data.words.map(w => ({
121
+ word: w.word,
122
+ start: w.start,
123
+ end: w.end
124
+ }));
125
+
126
+ // Get full text
127
+ const fullText = data.text;
128
+
129
+ // Process the text with existing logic
130
+ const processedBlocks = getProcessedBlocks(fullText);
131
+
132
+ // Map timestamps to processed blocks
133
+ const srtContent = generateSRT(processedBlocks, currentTranscriptWords);
134
+
135
+ // Display Results
136
+ outputText.value = srtContent; // Show SRT format in textarea or just text? Let's show SRT content so they can see timing
137
+ blockCount.textContent = processedBlocks.length;
138
+ currentSrtData = srtContent;
139
+ downloadSrtBtn.classList.remove('hidden');
140
+
141
+ showToast("Transcription & Alignment complete!");
142
+
143
+ } catch (error) {
144
+ console.error(error);
145
+ showToast(error.message, "error");
146
+ } finally {
147
+ transcribeBtn.disabled = false;
148
+ transcribingStatus.classList.add('hidden');
149
+ }
150
+ }
151
+
152
+ transcribeBtn.addEventListener('click', handleTranscription);
153
+
154
+ // --- SRT GENERATION ---
155
+
156
+ function formatSRTTime(seconds) {
157
+ const date = new Date(0);
158
+ date.setMilliseconds(seconds * 1000);
159
+ const isoString = date.toISOString();
160
+ // Extract HH:MM:SS,ms
161
+ return isoString.substr(11, 8) + ',' + isoString.substr(20, 3);
162
+ }
163
+
164
+ function generateSRT(blocks, words) {
165
+ let srtOutput = "";
166
+ let wordIndex = 0;
167
+ let blockIndex = 1;
168
+
169
+ // Normalize punctuation in blocks to match Whisper words (roughly)
170
+ // Whisper usually returns words without punctuation attached, or with basic punctuation
171
+ // Our script adds "!" for commas.
172
+
173
+ for (const block of blocks) {
174
+ // Split block into words (removing our added punctuation for matching)
175
+ // We need to reconstruct the text for display but match based on content
176
+
177
+ // Simple approach: Count words in the block
178
+ // Calculate how many whisper words correspond to this block text
179
+ const blockWords = block.replace(/[!?.,]/g, '').trim().split(/\s+/).filter(w => w.length > 0);
180
+ const numWords = blockWords.length;
181
+
182
+ if (numWords === 0) continue;
183
+
184
+ if (wordIndex >= words.length) break;
185
+
186
+ // Determine start and end time
187
+ // Start is the start of the first word in this chunk
188
+ const startTime = words[wordIndex].start;
189
+
190
+ // End is the end of the last word in this chunk
191
+ // Look ahead 'numWords - 1'
192
+ let endIndex = wordIndex + numWords - 1;
193
+ if (endIndex >= words.length) endIndex = words.length - 1;
194
+ const endTime = words[endIndex].end;
195
+
196
+ // Format Entry
197
+ srtOutput += `${blockIndex}\n`;
198
+ srtOutput += `${formatSRTTime(startTime)} --> ${formatSRTTime(endTime)}\n`;
199
+ srtOutput += `${block}\n\n`;
200
+
201
+ wordIndex += numWords;
202
+ blockIndex++;
203
+ }
204
+
205
+ return srtOutput;
206
+ }
207
+
208
+ downloadSrtBtn.addEventListener('click', () => {
209
+ if (!currentSrtData) return;
210
+ const blob = new Blob([currentSrtData], { type: 'text/plain' });
211
+ const url = URL.createObjectURL(blob);
212
+ const a = document.createElement('a');
213
+ a.href = url;
214
+ a.download = 'capcut_aligned.srt';
215
+ document.body.appendChild(a);
216
+ a.click();
217
+ document.body.removeChild(a);
218
+ URL.revokeObjectURL(url);
219
+ showToast("SRT file downloaded!");
220
+ });
221
 
222
  // --- UTILITIES ---
223
 
224
  function showToast(message, type = 'success') {
225
+ const toast = document.createElement('div');
226
  const bgColor = type === 'success' ? 'bg-green-500' : 'bg-red-500';
227
  toast.className = `fixed bottom-5 right-5 ${bgColor} text-white px-6 py-3 rounded-lg shadow-lg flex items-center gap-2 z-50 toast`;
228
  toast.innerHTML = `<i data-feather="${type === 'success' ? 'check-circle' : 'alert-circle'}"></i> ${message}`;
 
268
  return block.replace(/\s/g, '').length;
269
  }
270
 
271
+ // Separate the text processing logic to be reusable by both text and audio modes
272
+ function getProcessedBlocks(raw) {
 
 
 
 
 
273
  const cleanedText = cleanText(raw);
 
 
 
 
274
  const words = cleanedText.split(' ');
275
  let lines = [];
276
  let currentLine = [];
 
280
  let word = words[i];
281
  let nextWord = words[i + 1] || '';
282
 
283
+ const endsWithPunctuation = /[!?]|(\.\.)/.test(word.slice(-1));
 
 
 
 
284
  const isConnective = CONNECTIVES.has(word.toLowerCase().replace(/[!?.,]/g, ''));
285
 
 
286
  let startNewLine = false;
287
 
288
  if (isConnective && currentLine.length > 0) {
289
  startNewLine = true;
290
  }
291
 
 
 
 
292
  const proposedLineStr = [...currentLine, word].join(' ');
293
  const proposedLen = countCharsNoSpaces(proposedLineStr);
294
 
 
 
 
295
  if (currentLine.length > 0 && proposedLen > MAX_CHARS) {
296
  startNewLine = true;
297
  }
 
303
  currentLine.push(word);
304
  }
305
 
 
306
  if (endsWithPunctuation) {
307
  lines.push(currentLine.join(' '));
308
  currentLine = [];
 
313
  lines.push(currentLine.join(' '));
314
  }
315
 
316
+ // Anti-Weakening
 
 
 
 
 
 
 
 
 
 
317
  let changed = true;
 
318
  let iterations = 0;
319
 
320
  while (changed && iterations < 100) {
 
326
  const lastWord = lineWords[lineWords.length - 1];
327
 
328
  if (isWeakEnding(lastWord)) {
 
329
  const remainingWords = lineWords.slice(0, lineWords.length - 1);
330
 
331
  if (remainingWords.length === 0) {
332
+ lines[i] = lines[i+1];
 
 
 
 
 
 
333
  } else {
334
  lines[i] = remainingWords.join(' ');
335
  }
336
 
 
 
337
  const nextLineWords = lines[i+1].split(' ');
338
  lines[i+1] = [lastWord, ...nextLineWords].join(' ');
339
 
 
340
  if (lines[i].trim() === '') {
341
  lines.splice(i, 1);
342
+ i--;
343
  }
344
 
345
  changed = true;
 
347
  }
348
  }
349
 
350
+ return lines.filter(l => l.trim().length > 0);
351
+ }
352
+
353
+ function processScript() {
354
+ const raw = inputText.value;
355
+ if (!raw.trim()) {
356
+ showToast("Please enter text to process.", "error");
357
+ return;
358
+ }
359
 
360
+ const lines = getProcessedBlocks(raw);
361
+ blockCount.textContent = lines.length;
 
362
 
363
  showToast("Script processed successfully!");
364
  }
style.css CHANGED
@@ -1,6 +1,5 @@
1
  /* Base styles are handled by Tailwind CSS in the head */
2
  /* Custom overrides are handled in index.html <style> */
3
-
4
  /* Animation for Toast Notification */
5
  @keyframes slideIn {
6
  from { transform: translateY(100%); opacity: 0; }
@@ -12,10 +11,25 @@
12
  to { opacity: 0; }
13
  }
14
 
 
 
 
 
15
  .toast {
16
  animation: slideIn 0.3s ease-out forwards;
17
  }
18
 
19
  .toast.hiding {
20
  animation: fadeOut 0.3s ease-in forwards;
21
- }
 
 
 
 
 
 
 
 
 
 
 
 
1
  /* Base styles are handled by Tailwind CSS in the head */
2
  /* Custom overrides are handled in index.html <style> */
 
3
  /* Animation for Toast Notification */
4
  @keyframes slideIn {
5
  from { transform: translateY(100%); opacity: 0; }
 
11
  to { opacity: 0; }
12
  }
13
 
14
+ @keyframes spin {
15
+ to { transform: rotate(360deg); }
16
+ }
17
+
18
  .toast {
19
  animation: slideIn 0.3s ease-out forwards;
20
  }
21
 
22
  .toast.hiding {
23
  animation: fadeOut 0.3s ease-in forwards;
24
+ }
25
+
26
+ .animate-spin {
27
+ animation: spin 1s linear infinite;
28
+ }
29
+
30
+ /* Mode Toggle Active State */
31
+ .mode-active {
32
+ background-color: #334155; /* Slate 700 */
33
+ color: white;
34
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
35
+ }