Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Transcribe JSON to SRT Converter</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #4a6fa5; | |
| --secondary-color: #166088; | |
| --accent-color: #4fc3f7; | |
| --background-color: #f8f9fa; | |
| --text-color: #333; | |
| --light-gray: #e9ecef; | |
| --dark-gray: #6c757d; | |
| --success-color: #28a745; | |
| --error-color: #dc3545; | |
| --border-radius: 8px; | |
| --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| --transition: all 0.3s ease; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| line-height: 1.6; | |
| color: var(--text-color); | |
| background-color: var(--background-color); | |
| padding: 20px; | |
| min-height: 100vh; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 30px; | |
| padding-bottom: 20px; | |
| border-bottom: 1px solid var(--light-gray); | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .logo i { | |
| font-size: 24px; | |
| color: var(--primary-color); | |
| } | |
| .logo span { | |
| font-size: 20px; | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| } | |
| .anycoder-link { | |
| font-size: 14px; | |
| color: var(--dark-gray); | |
| text-decoration: none; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--primary-color); | |
| } | |
| .converter-card { | |
| background: white; | |
| border-radius: var(--border-radius); | |
| box-shadow: var(--box-shadow); | |
| padding: 30px; | |
| margin-bottom: 30px; | |
| } | |
| h1 { | |
| color: var(--secondary-color); | |
| margin-bottom: 20px; | |
| font-size: 28px; | |
| text-align: center; | |
| } | |
| .description { | |
| text-align: center; | |
| color: var(--dark-gray); | |
| margin-bottom: 30px; | |
| font-size: 16px; | |
| } | |
| .input-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .input-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| label { | |
| font-weight: 500; | |
| color: var(--secondary-color); | |
| font-size: 14px; | |
| } | |
| textarea, input { | |
| padding: 12px; | |
| border: 1px solid var(--light-gray); | |
| border-radius: var(--border-radius); | |
| font-family: inherit; | |
| font-size: 14px; | |
| transition: var(--transition); | |
| } | |
| textarea:focus, input:focus { | |
| outline: none; | |
| border-color: var(--accent-color); | |
| box-shadow: 0 0 0 2px rgba(79, 195, 247, 0.2); | |
| } | |
| .word-break-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .word-break-input { | |
| width: 80px; | |
| } | |
| .buttons { | |
| display: flex; | |
| gap: 15px; | |
| margin-top: 20px; | |
| } | |
| button { | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: var(--border-radius); | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .convert-btn { | |
| background-color: var(--primary-color); | |
| color: white; | |
| } | |
| .convert-btn:hover { | |
| background-color: var(--secondary-color); | |
| transform: translateY(-2px); | |
| } | |
| .clear-btn { | |
| background-color: var(--light-gray); | |
| color: var(--text-color); | |
| } | |
| .clear-btn:hover { | |
| background-color: var(--dark-gray); | |
| color: white; | |
| } | |
| .output-section { | |
| margin-top: 30px; | |
| } | |
| .output-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| } | |
| .output-title { | |
| font-weight: 600; | |
| color: var(--secondary-color); | |
| } | |
| .copy-btn { | |
| background-color: var(--accent-color); | |
| color: white; | |
| padding: 8px 16px; | |
| } | |
| .copy-btn:hover { | |
| background-color: #3ba8e6; | |
| } | |
| #output { | |
| width: 100%; | |
| min-height: 200px; | |
| padding: 15px; | |
| border: 1px solid var(--light-gray); | |
| border-radius: var(--border-radius); | |
| font-family: 'Courier New', Courier, monospace; | |
| font-size: 14px; | |
| background-color: #f8f9fa; | |
| white-space: pre-wrap; | |
| overflow-x: auto; | |
| } | |
| .status-message { | |
| margin-top: 20px; | |
| padding: 12px; | |
| border-radius: var(--border-radius); | |
| display: none; | |
| } | |
| .success { | |
| background-color: rgba(40, 167, 69, 0.1); | |
| color: var(--success-color); | |
| border: 1px solid rgba(40, 167, 69, 0.2); | |
| } | |
| .error { | |
| background-color: rgba(220, 53, 69, 0.1); | |
| color: var(--error-color); | |
| border: 1px solid rgba(220, 53, 69, 0.2); | |
| } | |
| .example-section { | |
| margin-top: 40px; | |
| padding: 20px; | |
| background-color: white; | |
| border-radius: var(--border-radius); | |
| box-shadow: var(--box-shadow); | |
| } | |
| .example-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| } | |
| .example-title { | |
| font-weight: 600; | |
| color: var(--secondary-color); | |
| } | |
| .example-json { | |
| background-color: #f8f9fa; | |
| padding: 15px; | |
| border-radius: var(--border-radius); | |
| font-family: 'Courier New', Courier, monospace; | |
| font-size: 13px; | |
| white-space: pre-wrap; | |
| border: 1px solid var(--light-gray); | |
| } | |
| .toggle-example { | |
| background: none; | |
| border: none; | |
| color: var(--primary-color); | |
| cursor: pointer; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .toggle-example:hover { | |
| text-decoration: underline; | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 10px; | |
| } | |
| .converter-card { | |
| padding: 20px; | |
| } | |
| .buttons { | |
| flex-direction: column; | |
| } | |
| button { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| .word-break-control { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .word-break-input { | |
| width: 100%; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| h1 { | |
| font-size: 24px; | |
| } | |
| .description { | |
| font-size: 14px; | |
| } | |
| textarea, input { | |
| font-size: 13px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div class="logo"> | |
| <i class="fas fa-robot"></i> | |
| <span>Transcribe Converter</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" class="anycoder-link" target="_blank">Built with anycoder</a> | |
| </header> | |
| <div class="converter-card"> | |
| <h1>JSON to SRT Converter</h1> | |
| <p class="description">Convert Amazon Transcribe JSON format to SRT subtitles with configurable segmentation</p> | |
| <div class="input-section"> | |
| <div class="input-group"> | |
| <label for="jsonInput">Transcribe JSON Input</label> | |
| <textarea id="jsonInput" rows="10" placeholder='Paste your Transcribe JSON here. Example format: | |
| { | |
| "text": "Full transcription text...", | |
| "chunks": [ | |
| { | |
| "text": "First segment...", | |
| "start": 0.0, | |
| "end": 2.5 | |
| }, | |
| ... | |
| ] | |
| }'></textarea> | |
| </div> | |
| <div class="input-group"> | |
| <label>Segmentation Settings</label> | |
| <div class="word-break-control"> | |
| <span>Word break threshold:</span> | |
| <input type="number" id="wordBreakThreshold" class="word-break-input" min="1" max="50" value="10"> | |
| <span>words</span> | |
| </div> | |
| <p style="font-size: 12px; color: var(--dark-gray); margin-top: 5px;"> | |
| Subtitles will break at sentence boundaries or when reaching this word count | |
| </p> | |
| </div> | |
| </div> | |
| <div class="buttons"> | |
| <button class="convert-btn" id="convertBtn"> | |
| <i class="fas fa-exchange-alt"></i> | |
| Convert to SRT | |
| </button> | |
| <button class="clear-btn" id="clearBtn"> | |
| <i class="fas fa-times"></i> | |
| Clear All | |
| </button> | |
| </div> | |
| <div class="output-section"> | |
| <div class="output-header"> | |
| <div class="output-title">SRT Output</div> | |
| <button class="copy-btn" id="copyBtn"> | |
| <i class="fas fa-copy"></i> | |
| Copy to Clipboard | |
| </button> | |
| </div> | |
| <div id="output" readonly></div> | |
| </div> | |
| <div class="status-message" id="statusMessage"></div> | |
| </div> | |
| <div class="example-section"> | |
| <div class="example-header"> | |
| <div class="example-title">Example JSON Input</div> | |
| <button class="toggle-example" id="toggleExample"> | |
| <i class="fas fa-chevron-down"></i> | |
| Show Example | |
| </button> | |
| </div> | |
| <div class="example-json" id="exampleJson" style="display: none;"> | |
| { | |
| "text": "Hello world. This is a test transcription. We're going to demonstrate how this converter works with multiple sentences and different timing segments. Each sentence should ideally become its own subtitle segment.", | |
| "chunks": [ | |
| { | |
| "text": "Hello world.", | |
| "start": 0.0, | |
| "end": 1.2 | |
| }, | |
| { | |
| "text": "This is a test transcription.", | |
| "start": 1.2, | |
| "end": 3.5 | |
| }, | |
| { | |
| "text": "We're going to demonstrate", | |
| "start": 3.5, | |
| "end": 5.8 | |
| }, | |
| { | |
| "text": "how this converter works", | |
| "start": 5.8, | |
| "end": 7.2 | |
| }, | |
| { | |
| "text": "with multiple sentences", | |
| "start": 7.2, | |
| "end": 8.9 | |
| }, | |
| { | |
| "text": "and different timing segments.", | |
| "start": 8.9, | |
| "end": 10.5 | |
| }, | |
| { | |
| "text": "Each sentence should ideally become its own subtitle segment.", | |
| "start": 10.5, | |
| "end": 13.8 | |
| } | |
| ] | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const jsonInput = document.getElementById('jsonInput'); | |
| const wordBreakThreshold = document.getElementById('wordBreakThreshold'); | |
| const convertBtn = document.getElementById('convertBtn'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const copyBtn = document.getElementById('copyBtn'); | |
| const output = document.getElementById('output'); | |
| const statusMessage = document.getElementById('statusMessage'); | |
| const toggleExample = document.getElementById('toggleExample'); | |
| const exampleJson = document.getElementById('exampleJson'); | |
| // Toggle example visibility | |
| toggleExample.addEventListener('click', function() { | |
| const isVisible = exampleJson.style.display === 'block'; | |
| exampleJson.style.display = isVisible ? 'none' : 'block'; | |
| toggleExample.innerHTML = ` | |
| <i class="fas fa-chevron-${isVisible ? 'down' : 'up'}"></i> | |
| ${isVisible ? 'Show Example' : 'Hide Example'} | |
| `; | |
| }); | |
| // Clear all inputs and outputs | |
| clearBtn.addEventListener('click', function() { | |
| jsonInput.value = ''; | |
| output.value = ''; | |
| statusMessage.style.display = 'none'; | |
| }); | |
| // Copy output to clipboard | |
| copyBtn.addEventListener('click', function() { | |
| if (output.value.trim() === '') { | |
| showStatus('No content to copy', 'error'); | |
| return; | |
| } | |
| navigator.clipboard.writeText(output.value) | |
| .then(() => { | |
| showStatus('SRT content copied to clipboard!', 'success'); | |
| }) | |
| .catch(err => { | |
| showStatus('Failed to copy: ' + err, 'error'); | |
| }); | |
| }); | |
| // Convert JSON to SRT | |
| convertBtn.addEventListener('click', function() { | |
| try { | |
| const jsonText = jsonInput.value.trim(); | |
| if (!jsonText) { | |
| throw new Error('Please provide JSON input'); | |
| } | |
| const jsonData = JSON.parse(jsonText); | |
| validateJsonStructure(jsonData); | |
| const threshold = parseInt(wordBreakThreshold.value); | |
| if (isNaN(threshold) || threshold < 1 || threshold > 50) { | |
| throw new Error('Word break threshold must be between 1 and 50'); | |
| } | |
| const srtContent = convertToSRT(jsonData, threshold); | |
| output.value = srtContent; | |
| showStatus('Conversion successful!', 'success'); | |
| } catch (error) { | |
| showStatus('Error: ' + error.message, 'error'); | |
| console.error(error); | |
| } | |
| }); | |
| // Validate JSON structure | |
| function validateJsonStructure(jsonData) { | |
| if (!jsonData || typeof jsonData !== 'object') { | |
| throw new Error('Invalid JSON structure'); | |
| } | |
| if (!jsonData.text || typeof jsonData.text !== 'string') { | |
| throw new Error('Missing or invalid "text" field'); | |
| } | |
| if (!jsonData.chunks || !Array.isArray(jsonData.chunks) || jsonData.chunks.length === 0) { | |
| throw new Error('Missing or invalid "chunks" array'); | |
| } | |
| for (let i = 0; i < jsonData.chunks.length; i++) { | |
| const chunk = jsonData.chunks[i]; | |
| if (!chunk.text || typeof chunk.text !== 'string') { | |
| throw new Error(`Chunk ${i + 1} missing or invalid "text" field`); | |
| } | |
| if (typeof chunk.start !== 'number' || typeof chunk.end !== 'number') { | |
| throw new Error(`Chunk ${i + 1} missing or invalid timestamp fields`); | |
| } | |
| if (chunk.start < 0 || chunk.end <= chunk.start) { | |
| throw new Error(`Chunk ${i + 1} has invalid timestamp values`); | |
| } | |
| } | |
| } | |
| // Convert JSON to SRT format | |
| function convertToSRT(jsonData, wordThreshold) { | |
| let srt = ''; | |
| let currentSegment = { | |
| text: '', | |
| startTime: null, | |
| endTime: null, | |
| wordCount: 0 | |
| }; | |
| let segmentNumber = 1; | |
| // Process each chunk | |
| for (const chunk of jsonData.chunks) { | |
| const chunkText = chunk.text.trim(); | |
| if (!chunkText) continue; | |
| // Split chunk text into sentences (simple approach) | |
| const sentences = splitIntoSentences(chunkText); | |
| for (const sentence of sentences) { | |
| const words = sentence.split(/\s+/).filter(word => word.length > 0); | |
| const wordCount = words.length; | |
| // Check if we need to start a new segment | |
| if (currentSegment.startTime === null || | |
| (currentSegment.wordCount + wordCount > wordThreshold && | |
| currentSegment.wordCount > 0)) { | |
| // Save previous segment if it exists | |
| if (currentSegment.startTime !== null) { | |
| srt += formatSRTSegment(segmentNumber++, currentSegment); | |
| } | |
| // Start new segment | |
| currentSegment = { | |
| text: sentence, | |
| startTime: chunk.start, | |
| endTime: chunk.end, | |
| wordCount: wordCount | |
| }; | |
| } else { | |
| // Add to current segment | |
| currentSegment.text += ' ' + sentence; | |
| currentSegment.endTime = chunk.end; | |
| currentSegment.wordCount += wordCount; | |
| } | |
| } | |
| } | |
| // Add the last segment | |
| if (currentSegment.startTime !== null) { | |
| srt += formatSRTSegment(segmentNumber, currentSegment); | |
| } | |
| return srt; | |
| } | |
| // Simple sentence splitting (can be enhanced) | |
| function splitIntoSentences(text) { | |
| // Split on common sentence terminators followed by whitespace or end of string | |
| return text.split(/[.!?]+(?=\s|$)/) | |
| .map(s => s.trim()) | |
| .filter(s => s.length > 0); | |
| } | |
| // Format a single SRT segment | |
| function formatSRTSegment(number, segment) { | |
| return `${number}\n` + | |
| `${formatTime(segment.startTime)} --> ${formatTime(segment.endTime)}\n` + | |
| `${segment.text.trim()}\n\n`; | |
| } | |
| // Format time in SRT format (HH:MM:SS,mmm) | |
| function formatTime(seconds) { | |
| if (seconds === null || seconds === undefined) return '00:00:00,000'; | |
| const date = new Date(seconds * 1000); | |
| const hours = String(date.getUTCHours()).padStart(2, '0'); | |
| const minutes = String(date.getUTCMinutes()).padStart(2, '0'); | |
| const secs = String(date.getUTCSeconds()).padStart(2, '0'); | |
| const millis = String(Math.floor(date.getUTCMilliseconds())).padStart(3, '0'); | |
| return `${hours}:${minutes}:${secs},${millis}`; | |
| } | |
| // Show status message | |
| function showStatus(message, type) { | |
| statusMessage.textContent = message; | |
| statusMessage.className = 'status-message ' + type; | |
| statusMessage.style.display = 'block'; | |
| // Hide after 5 seconds | |
| setTimeout(() => { | |
| statusMessage.style.display = 'none'; | |
| }, 5000); | |
| } | |
| // Load example JSON when clicking the example button | |
| document.getElementById('exampleJson').addEventListener('click', function() { | |
| if (jsonInput.value.trim() === '') { | |
| jsonInput.value = this.textContent.trim(); | |
| showStatus('Example JSON loaded!', 'success'); | |
| } | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |