{"id":57,"date":"2025-07-30T02:24:11","date_gmt":"2025-07-29T18:24:11","guid":{"rendered":"https:\/\/blog.liu-qi.cn\/?p=57"},"modified":"2025-07-30T02:24:11","modified_gmt":"2025-07-29T18:24:11","slug":"%e5%bd%95%e9%9f%b3%e8%bd%ac%e6%96%87%e5%ad%97","status":"publish","type":"post","link":"https:\/\/en.blog.liu-qi.cn\/2025\/07\/30\/%e5%bd%95%e9%9f%b3%e8%bd%ac%e6%96%87%e5%ad%97\/","title":{"rendered":"Recordings to Text Conversion Tool"},"content":{"rendered":"\n<pre class=\"wp-block-code\"><code>&lt;!DOCTYPE html&gt;\n&lt;html lang=\"zh-CN\"&gt;\n&lt;head&gt;\n    &lt;meta charset=\"UTF-8\"&gt;\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"&gt;\n    &lt;title&gt;Audio to Text&lt;\/title&gt;\n    &lt;style&gt;\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body {\n            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n            min-height: 100vh;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            padding: 20px;\n        }\n\n        .container {\n            background: rgba(255, 255, 255, 0.95);\n            backdrop-filter: blur(10px);\n            border-radius: 20px;\n            padding: 40px;\n            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);\n            max-width: 700px;\n            width: 100%;\n        }\n\n        h1 {\n            text-align: center;\n            color: #333;\n            margin-bottom: 30px;\n            font-size: 2.5em;\n            background: linear-gradient(45deg, #667eea, #764ba2);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n        }\n\n        .config-section {\n            margin-bottom: 30px;\n            padding: 20px;\n            background: rgba(102, 126, 234, 0.1);\n            border-radius: 15px;\n            border-left: 4px solid #667eea;\n        }\n\n        .config-section h3 {\n            color: #333;\n            margin-bottom: 15px;\n        }\n\n        .input-group {\n            margin-bottom: 15px;\n        }\n\n        label {\n            display: block;\n            color: #555;\n            margin-bottom: 5px;\n            font-weight: 500;\n        }\n\n        input, select {\n            width: 100%;\n            padding: 12px;\n            border: 2px solid #ddd;\n            border-radius: 10px;\n            font-size: 16px;\n            transition: all 0.3s ease;\n        }\n\n        input:focus, select:focus {\n            outline: none;\n            border-color: #667eea;\n            box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);\n        }\n\n        small {\n            display: block;\n            margin-top: 5px;\n            color: #666;\n            font-size: 12px;\n        }\n\n        .record-section {\n            text-align: center;\n            margin: 30px 0;\n        }\n\n        .record-btn {\n            background: linear-gradient(45deg, #667eea, #764ba2);\n            color: white;\n            border: none;\n            padding: 20px 40px;\n            border-radius: 50px;\n            font-size: 18px;\n            font-weight: bold;\n            cursor: pointer;\n            transition: all 0.3s ease;\n            margin: 10px;\n            box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);\n        }\n\n        .record-btn:hover {\n            transform: translateY(-3px);\n            box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4);\n        }\n\n        .record-btn:active {\n            transform: translateY(0);\n        }\n\n        .record-btn.recording {\n            background: linear-gradient(45deg, #ff6b6b, #ee5a52);\n            animation: pulse 2s infinite;\n        }\n\n        @keyframes pulse {\n            0% { transform: scale(1); }\n            50% { transform: scale(1.05); }\n            100% { transform: scale(1); }\n        }\n\n        .status {\n            margin: 20px 0;\n            padding: 15px;\n            border-radius: 10px;\n            text-align: center;\n            font-weight: 500;\n        }\n\n        .status.info {\n            background: rgba(52, 152, 219, 0.1);\n            color: #2980b9;\n            border: 1px solid rgba(52, 152, 219, 0.3);\n        }\n\n        .status.success {\n            background: rgba(46, 204, 113, 0.1);\n            color: #27ae60;\n            border: 1px solid rgba(46, 204, 113, 0.3);\n        }\n\n        .status.error {\n            background: rgba(231, 76, 60, 0.1);\n            color: #c0392b;\n            border: 1px solid rgba(231, 76, 60, 0.3);\n        }\n\n        .result-section {\n            margin-top: 30px;\n        }\n\n        .result-text {\n            background: #f8f9fa;\n            border: 2px solid #e9ecef;\n            border-radius: 15px;\n            padding: 20px;\n            min-height: 120px;\n            font-size: 16px;\n            line-height: 1.6;\n            white-space: pre-wrap;\n            word-wrap: break-word;\n        }\n\n        .audio-player {\n            margin: 20px 0;\n            width: 100%;\n        }\n\n        .file-upload {\n            margin: 20px 0;\n            text-align: center;\n        }\n\n        .file-upload input&#091;type=\"file\"] {\n            display: none;\n        }\n\n        .file-upload label {\n            display: inline-block;\n            padding: 15px 30px;\n            background: linear-gradient(45deg, #28a745, #20c997);\n            color: white;\n            border-radius: 25px;\n            cursor: pointer;\n            transition: all 0.3s ease;\n            font-weight: bold;\n        }\n\n        .file-upload label:hover {\n            transform: translateY(-2px);\n            box-shadow: 0 10px 25px rgba(40, 167, 69, 0.3);\n        }\n\n        .speaker-timeline {\n            margin: 20px 0;\n            padding: 15px;\n            background: #f8f9fa;\n            border-radius: 10px;\n            border-left: 4px solid #667eea;\n        }\n\n        .speaker-segment {\n            margin: 8px 0;\n            padding: 10px;\n            border-radius: 8px;\n            position: relative;\n        }\n\n        .speaker-1 {\n            background: rgba(102, 126, 234, 0.1);\n            border-left: 3px solid #667eea;\n        }\n\n        .speaker-2 {\n            background: rgba(255, 107, 107, 0.1);\n            border-left: 3px solid #ff6b6b;\n        }\n\n        .speaker-3 {\n            background: rgba(46, 204, 113, 0.1);\n            border-left: 3px solid #2ecc71;\n        }\n\n        .speaker-4 {\n            background: rgba(241, 196, 15, 0.1);\n            border-left: 3px solid #f1c40f;\n        }\n\n        .speaker-label {\n            font-weight: bold;\n            color: #555;\n            margin-bottom: 5px;\n            font-size: 14px;\n        }\n\n        .speaker-time {\n            font-size: 12px;\n            color: #888;\n            margin-left: 10px;\n        }\n\n        .speaker-text {\n            margin-top: 5px;\n            line-height: 1.4;\n        }\n\n        .analysis-section {\n            margin: 20px 0;\n            padding: 15px;\n            background: rgba(52, 152, 219, 0.1);\n            border-radius: 10px;\n            border-left: 4px solid #3498db;\n        }\n\n        .toggle-section {\n            margin: 10px 0;\n        }\n\n        .toggle-btn {\n            background: #3498db;\n            color: white;\n            border: none;\n            padding: 8px 16px;\n            border-radius: 5px;\n            cursor: pointer;\n            font-size: 14px;\n        }\n\n        .toggle-btn:hover {\n            background: #2980b9;\n        }\n    &lt;\/style&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n    &lt;div class=\"container\"&gt;\n        &lt;h1&gt;\ud83c\udf99\ufe0f Audio to Text&lt;\/h1&gt;\n        \n        &lt;!-- API Configuration Area --&gt;\n        &lt;div class=\"config-section\"&gt;\n            &lt;h3&gt;\u2699\ufe0f API Configuration&lt;\/h3&gt;\n            &lt;div class=\"input-group\"&gt;\n                &lt;label for=\"apiProvider\"&gt;API Provider:&lt;\/label&gt;\n                &lt;select id=\"apiProvider\"&gt;\n                    &lt;option value=\"siliconflow\"&gt;SiliconFlow (Pure Transcription)&lt;\/option&gt;\n                    &lt;option value=\"volcengine\"&gt;Volcengine (Supports Speaker Diarization)&lt;\/option&gt;\n                &lt;\/select&gt;\n            &lt;\/div&gt;\n            \n            &lt;!-- SiliconFlow Configuration --&gt;\n            &lt;div id=\"siliconflowConfig\"&gt;\n                &lt;div class=\"input-group\"&gt;\n                    &lt;label for=\"apiUrl\"&gt;API URL:&lt;\/label&gt;\n                    &lt;input type=\"text\" id=\"apiUrl\" value=\"https:\/\/api.siliconflow.cn\/v1\/audio\/transcriptions\"&gt;\n                &lt;\/div&gt;\n                &lt;div class=\"input-group\"&gt;\n                    &lt;label for=\"apiKey\"&gt;API Key:&lt;\/label&gt;\n                    &lt;input type=\"password\" id=\"apiKey\" placeholder=\"Please enter your SiliconFlow API key\"&gt;\n                &lt;\/div&gt;\n                &lt;div class=\"input-group\"&gt;\n                    &lt;label for=\"model\"&gt;Model:&lt;\/label&gt;\n                    &lt;input type=\"text\" id=\"model\" value=\"FunAudioLLM\/SenseVoiceSmall\"&gt;\n                &lt;\/div&gt;\n            &lt;\/div&gt;\n            \n            &lt;!-- Volcengine Configuration --&gt;\n            &lt;div id=\"volcengineConfig\" style=\"display: none;\"&gt;\n                &lt;div class=\"input-group\"&gt;\n                    &lt;label for=\"volcSubmitUrl\"&gt;Submit Task API:&lt;\/label&gt;\n                    &lt;input type=\"text\" id=\"volcSubmitUrl\" value=\"https:\/\/openspeech.bytedance.com\/api\/v3\/auc\/bigmodel\/submit\"&gt;\n                &lt;\/div&gt;\n                &lt;div class=\"input-group\"&gt;\n                    &lt;label for=\"volcQueryUrl\"&gt;Query Results API:&lt;\/label&gt;\n                    &lt;input type=\"text\" id=\"volcQueryUrl\" value=\"https:\/\/openspeech.bytedance.com\/api\/v3\/auc\/bigmodel\/query\"&gt;\n                &lt;\/div&gt;\n                &lt;div class=\"input-group\"&gt;\n                    &lt;label for=\"volcAppKey\"&gt;APP ID:&lt;\/label&gt;\n                    &lt;input type=\"text\" id=\"volcAppKey\" placeholder=\"APP ID obtained from Volcengine console\"&gt;\n                &lt;\/div&gt;\n                &lt;div class=\"input-group\"&gt;\n                    &lt;label for=\"volcAccessKey\"&gt;Access Token:&lt;\/label&gt;\n                    &lt;\n                &lt;\/div&gt;\n                \n                &lt;<!-- Audio URL acquisition method selection -->\n                &lt;div class=\"input-group\"&gt;\n                    &lt;<label for=\"audioUrlMethod\">Audio URL acquisition method:&lt;\/label&gt;\n                    &lt;select id=\"audioUrlMethod\"&gt;\n                        &lt;Directly input audio URL&lt;\/option&gt;\n                        &lt;Obtain URL via upload service&lt;\/option&gt;\n                    &lt;\/select&gt;\n                &lt;\/div&gt;\n                \n                &lt;<!-- Direct URL input option -->\n                &lt;div id=\"directUrlConfig\"&gt;\n                    &lt;div class=\"input-group\" style=\"background: rgba(46, 204, 113, 0.1); padding: 15px; border-radius: 8px; border-left: 3px solid #2ecc71;\"&gt;\n                        &lt;<label for=\"directAudioUrl\">Audio file URL:&lt;\/label&gt;\n                        &lt;input type=\"url\" id=\"directAudioUrl\" placeholder=\"https:\/\/example.com\/your-audio-file.mp3\"&gt;\n                        &lt;<small>Please enter the public direct link address of the audio file (a downloadable URL)&lt;\/small&gt;\n                        &lt;<button type=\"button\" id=\"validateUrlBtn\" style=\"margin-top: 8px;padding: 6px 12px;background: #3498db;color: white;border: none;border-radius: 4px;cursor: pointer;font-size: 12px\">\ud83d\udd0d Validate Link<\/button>&lt;\/button&gt;\n                        &lt;div id=\"urlValidationResult\" style=\"margin-top: 8px; display: none;\"&gt;&lt;\/div&gt;\n                        &lt;div style=\"margin-top: 10px; padding: 8px; background: rgba(255, 255, 255, 0.7); border-radius: 5px;\"&gt;\n                            &lt;<strong>\u2705 Valid direct link sources:<\/strong>&lt;\/strong&gt;&lt;br&gt;\n                            1. &lt;<strong>Your own website server<\/strong>&lt;<\/strong>: For example, https:\/\/yoursite.com\/audio\/file.mp3&lt;br&gt;\n                            2. &lt;<strong>GitHub Raw link<\/strong>&lt;<\/strong>: Upload to a GitHub repository and obtain the Raw file link&lt;br&gt;\n                            3. &lt;<strong>Professional CDN services<\/strong>&lt;<\/strong>: Such as public links from Alibaba Cloud OSS, Tencent Cloud COS, etc.&lt;br&gt;\n                            4. &lt;strong&gt;Free Direct Link Hosting&lt;\/strong&gt;: Use the \"Get URL via Upload Service\" below&lt;br&gt;\n                            &lt;br&gt;\n                            &lt;strong style=\"color: #e74c3c;\"&gt;\u274c Invalid Link (Not a Direct Link):&lt;\/strong&gt;&lt;br&gt;\n                            \u2022 Cloud storage sharing links like Baidu Netdisk, OneDrive, etc.&lt;br&gt;\n                            \u2022 Links requiring login or verification&lt;br&gt;\n                            \u2022 Redirect links&lt;br&gt;\n                            &lt;br&gt;\n                            &lt;small style=\"color: #666;\"&gt;&lt;strong&gt;Test Method:&lt;\/strong&gt;Open the URL directly in a browser; it should directly download\/play the audio file&lt;\/small&gt;\n                        &lt;\/div&gt;\n                    &lt;\/div&gt;\n                &lt;\/div&gt;\n                \n                &lt;!-- Upload Service Options --&gt;\n                &lt;div id=\"uploadServiceConfig\" style=\"display: none;\"&gt;\n                    &lt;div class=\"input-group\" style=\"background: rgba(40, 167, 69, 0.1); padding: 15px; border-radius: 8px; border-left: 3px solid #28a745;\"&gt;\n                        &lt;label&gt;\u2705 Free Direct Link Hosting Services:&lt;\/label&gt;\n                        &lt;p style=\"margin: 5px 0; color: #155724; font-size: 14px;\"&gt;\n                            &lt;strong&gt;Auto-upload and get direct links&lt;\/strong&gt;&lt;br&gt;\n                            \u2022 Supports services like catbox.moe, file.io, etc.&lt;br&gt;\n                            \u2022 Automatically generates direct links usable for the Volcano Engine API&lt;br&gt;\n                            \u2022 If you encounter network issues, try a few times or use a custom service&lt;br&gt;\n                        &lt;\/p&gt;\n                    &lt;\/div&gt;\n                    &lt;div class=\"input-group\"&gt;\n                        &lt;label for=\"customUploadUrl\"&gt;Custom Upload Service API (optional):&lt;\/label&gt;\n                        &lt;input type=\"text\" id=\"customUploadUrl\" placeholder=\"http:\/\/your-server.com\/upload\"&gt;\n                        &lt;small&gt;If you have your own file upload API, please enter it. The API should return JSON: {\"url\": \"direct link URL\"}&lt;\/small&gt;\n                    &lt;\/div&gt;\n                &lt;\/div&gt;\n                \n                &lt;div class=\"input-group\"&gt;\n                    &lt;label&gt;\n                        &lt;Enable speaker separation&lt;\/label&gt;\n                &lt;\/div&gt;\n            &lt;\/div&gt;\n        &lt;\/div&gt;\n\n        &lt;File upload area&lt;div class=\"file-upload\"&gt;\n            &lt;div id=\"fileUploadSection\"&gt;\n                &lt;Select audio file&lt;\/label&gt;\n                &lt;input type=\"file\" id=\"audioFile\" accept=\"audio\/*\"&gt;\n            &lt;\/div&gt;\n            \n            &lt;div id=\"directUrlSection\" style=\"display: none;\"&gt;\n                &lt;div style=\"text-align: center; padding: 20px; background: rgba(46, 204, 113, 0.1); border-radius: 15px; border: 2px dashed #2ecc71;\"&gt;\n                    &lt;Use direct URL mode&lt;\/p&gt;\n                    &lt;Please enter the audio file URL in the Volcengine configuration above&lt;\/p&gt;\n                &lt;\/div&gt;\n            &lt;\/div&gt;\n        &lt;\/div&gt;\n\n        &lt;Start conversion area&lt;div class=\"record-section\"&gt;\n            &lt;Start conversion&lt;\/button&gt;\n        &lt;\/div&gt;\n\n        &lt;Status display&lt;div id=\"status\" class=\"status\" style=\"display: none;\"&gt;&lt;\/div&gt;\n\n        &lt;Audio player&lt;audio id=\"audioPlayer\" class=\"audio-player\" controls style=\"display: none;\"&gt;&lt;\/audio&gt;\n\n        &lt;Result display area&lt;div class=\"result-section\"&gt;\n            &lt;div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;\"&gt;\n                &lt;Conversion result:&lt;\/h3&gt;\n                &lt;Export TXT&lt;\/button&gt;\n            &lt;\/div&gt;\n            \n            &lt;Conversion result text&lt;Speech-to-text results will be displayed here...&lt;\/div&gt;\n            \n            &lt;Audio analysis information&lt;div id=\"audioAnalysis\" class=\"analysis-section\" style=\"display: none;\"&gt;\n                &lt;Audio analysis:&lt;\/h4&gt;\n                &lt;div id=\"analysisInfo\"&gt;&lt;\/div&gt;\n            &lt;\/div&gt;\n        &lt;\/div&gt;\n    &lt;\/div&gt;\n\n    &lt;script&gt;\n        let currentAudioBlob;\n        let currentTranscription = ''; \/\/ Save the current conversion result\n        let currentSpeakerSegments = &#091;]; \/\/ Save the current speaker diarization results\n\n        const convertBtn = document.getElementById('convertBtn');\n        const exportBtn = document.getElementById('exportBtn');\n        const status = document.getElementById('status');\n        const resultText = document.getElementById('resultText');\n        const audioPlayer = document.getElementById('audioPlayer');\n        const audioFileInput = document.getElementById('audioFile');\n        const audioAnalysis = document.getElementById('audioAnalysis');\n        const analysisInfo = document.getElementById('analysisInfo');\n        const apiProvider = document.getElementById('apiProvider');\n        const siliconflowConfig = document.getElementById('siliconflowConfig');\n        const volcengineConfig = document.getElementById('volcengineConfig');\n        const audioUrlMethod = document.getElementById('audioUrlMethod');\n        const directUrlConfig = document.getElementById('directUrlConfig');\n        const uploadServiceConfig = document.getElementById('uploadServiceConfig');\n        const directAudioUrlInput = document.getElementById('directAudioUrl');\n        const customUploadUrlInput = document.getElementById('customUploadUrl');\n        const fileUploadSection = document.getElementById('fileUploadSection');\n        const directUrlSection = document.getElementById('directUrlSection');\n        const validateUrlBtn = document.getElementById('validateUrlBtn');\n        const urlValidationResult = document.getElementById('urlValidationResult');\n\n        \/\/ Switch API provider\n        function switchApiProvider() {\n            const provider = apiProvider.value;\n            if (provider === 'siliconflow') {\n                siliconflowConfig.style.display = 'block';\n                volcengineConfig.style.display = 'none';\n                \/\/ SiliconFlow only supports file upload\n                fileUploadSection.style.display = 'block';\n                directUrlSection.style.display = 'none';\n                \/\/ Reset Volc Engine configuration\n                directUrlConfig.style.display = 'none';\n                uploadServiceConfig.style.display = 'none';\n                customUploadUrlInput.value = '';\n                directAudioUrlInput.value = '';\n                audioUrlMethod.value = 'direct';\n            } else if (provider === 'volcengine') {\n                siliconflowConfig.style.display = 'none';\n                volcengineConfig.style.display = 'block';\n                \/\/ Reset SiliconFlow configuration\n                document.getElementById('apiUrl').value = 'https:\/\/api.siliconflow.cn\/v1\/audio\/transcriptions';\n                document.getElementById('apiKey').value = '';\n                document.getElementById('model').value = 'FunAudioLLM\/SenseVoiceSmall';\n\n                \/\/ Display configuration based on audio URL acquisition method\n                switchAudioUrlMethod();\n            }\n            \n            \/\/ Reset results display\n            resetResults();\n            \/\/ Reset convert button state\n            updateConvertButtonState();\n        }\n\n        \/\/ Switch audio URL acquisition method\n        function switchAudioUrlMethod() {\n            if (apiProvider.value !== 'volcengine') return;\n            \n            if (audioUrlMethod.value === 'direct') {\n                directUrlConfig.style.display = 'block';\n                uploadServiceConfig.style.display = 'none';\n                fileUploadSection.style.display = 'none';\n                directUrlSection.style.display = 'block';\n                customUploadUrlInput.value = '';\n            } else {\n                directUrlConfig.style.display = 'none';\n                uploadServiceConfig.style.display = 'block';\n                fileUploadSection.style.display = 'block';\n                directUrlSection.style.display = 'none';\n                directAudioUrlInput.value = '';\n            }\n            updateConvertButtonState();\n        }\n\n        \/\/ Update convert button state\n        function updateConvertButtonState() {\n            const provider = apiProvider.value;\n            let canConvert = false;\n            \n            if (provider === 'siliconflow') {\n                \/\/ SiliconFlow requires a file\n                canConvert = currentAudioBlob !== null;\n            } else if (provider === 'volcengine') {\n                if (audioUrlMethod.value === 'direct') {\n                    \/\/ Direct URL mode, check if URL is filled\n                    const url = directAudioUrlInput.value.trim();\n                    canConvert = url&amp;&amp; (url.startsWith('http:\/\/') || url.startsWith('https:\/\/'));\n                } else {\n                    \/\/ Upload service mode, file required\n                    canConvert = currentAudioBlob !== null;\n                }\n            }\n            \n            if (canConvert) {\n                convertBtn.style.display = 'inline-block';\n                if (provider === 'volcengine' &amp;&amp; audioUrlMethod.value === 'direct') {\n                    showStatus(`\ud83d\udd17 Direct URL configured, click \"Start Conversion\" for speech recognition`, 'success');\n                }\n            } else {\n                convertBtn.style.display = 'none';\n            }\n        }\n\n        \/\/ Reset results display\n        function resetResults() {\n            \/\/ speakerResults.style.display = 'none'; \/\/ Remove speaker results display\n            \/\/ originalResults.style.display = 'block'; \/\/ Remove full text display\n            audioAnalysis.style.display = 'none';\n            \/\/ toggleView.style.display = 'none'; \/\/ Remove view toggle button\n            exportBtn.style.display = 'none';\n            \/\/ currentViewMode = 'original'; \/\/ Remove view mode variable\n            currentTranscription = '';\n            currentSpeakerSegments = &#091;];\n            resultText.textContent = 'The speech-to-text result will be displayed here...';\n        }\n\n        \/\/ Display status message\n        function showStatus(message, type = 'info') {\n            status.textContent = message;\n            status.className = `status ${type}`;\n            status.style.display = 'block';\n            console.log(`&#091;${type.toUpperCase()}] ${message}`); \/\/ Add console log\n        }\n\n        \/\/ Hide status message\n        function hideStatus() {\n            status.style.display = 'none';\n        }\n\n        \/\/ Export as TXT file\n        function exportToTxt() {\n            let content = '';\n            const filename = `Speech-to-Text_${new Date().toISOString().slice(0, 19).replace(\/:\/g, '-')}.txt`;\n            \n            if (currentSpeakerSegments.length &gt; 0) {\n                \/\/ Has speaker diarization results, export diarization results + full text\n                content = '=== Speaker Diarization Results ===nn';\n                currentSpeakerSegments.forEach((segment) =&gt; {\n                    content += `&#091;Speaker ${segment.speaker}] (${formatTime(segment.startTime)} - ${formatTime(segment.endTime)})n`;\n                    content += `${segment.text}nn`;\n                });\n                content += `n=== Full Text ===nn${currentTranscription}`;\n            } else {\n                \/\/ No speaker separation results, export full text\n                content = currentTranscription;\n            }\n            \n            if (!content.trim()) {\n                showStatus('\u26a0\ufe0f No content available for export', 'error');\n                return;\n            }\n            \n            \/\/ Create download link\n            const blob = new Blob(&#091;content], { type: 'text\/plain;charset=utf-8' });\n            const url = URL.createObjectURL(blob);\n            const a = document.createElement('a');\n            a.href = url;\n            a.download = filename;\n            document.body.appendChild(a);\n            a.click();\n            document.body.removeChild(a);\n            URL.revokeObjectURL(url);\n            \n            showStatus('\ud83d\udcc4 File exported successfully!', 'success');\n        }\n\n        \/\/ Upload file to hosting service (improved version)\n        async function uploadToCatbox(file) {\n            showStatus('\ud83d\udce4 Trying free hosting service...', 'info');\n            \n            \/\/ List of available hosting services\n            const uploadServices = &#091;\n                {\n                    name: 'catbox.moe',\n                    upload: async (file) =&gt; {\n                        const formData = new FormData();\n                        formData.append('reqtype', 'fileupload');\n                        formData.append('fileToUpload', file);\n                        \n                        const response = await fetch('https:\/\/catbox.moe\/user\/api.php', {\n                            method: 'POST',\n                            body: formData\n                        });\n                        \n                        if (response.ok) {\n                            const result = await response.text();\n                            if (result.startsWith('https:\/\/files.catbox.moe\/')) {\n                                return result.trim();\n                            }\n                        }\n                        throw new Error('catbox.moe upload failed');\n                    }\n                },\n                {\n                    name: 'file.io',\n                    upload: async (file) =&gt; {\n                        const formData = new FormData();\n                        formData.append('file', file);\n                        \n                        const response = await fetch('https:\/\/file.io', {\n                            method: 'POST',\n                            body: formData\n                        });\n                        \n                        if (response.ok) {\n                            const result = await response.json();\n                            if (result.success&amp;&amp;result.link) {\n                                return result.link;\n                            }\n                        }\n                        throw new Error('file.io upload failed');\n                    }\n                }\n            ];\n            \n            let lastError = null;\n            \n            \/\/ Try each service\n            for (let i = 0; i&lt; uploadServices.length; i++) {\n                const service = uploadServices&#091;i];\n                try {\n                    showStatus(`\ud83d\udce4 Trying ${service.name}... (${i + 1}\/${uploadServices.length})`, 'info');\n                    console.log(`Trying to upload to: ${service.name}`);\n                    \n                    const url = await service.upload(file);\n                    console.log(`${service.name} upload successful:`, url);\n                    \n                    showStatus(`\u2705 File uploaded successfully! Using service: ${service.name}`, 'success');\n                    return url;\n                    \n                } catch (error) {\n                    console.warn(`${service.name} upload failed:`, error.message);\n                    lastError = error;\n                    \n                    if (i&lt;uploadServices.length - 1) {\n                        showStatus(`\u26a0\ufe0f ${service.name} failed, trying the next service...`, 'info');\n                        await new Promise(resolve =&gt; setTimeout(resolve, 1000));\n                    }\n                }\n            }\n            \n            \/\/ All services failed\n            console.error('All free hosting services failed');\n            showStatus(`\u274c Free hosting service is temporarily unavailable`, 'error');\n            \n            \/\/ Provide detailed solutions\n            const errorMessage = `\nFree hosting service is temporarily unavailable, please try the following solutions:\n\n\u2705 Recommended solutions:\n1. Use your own server to upload audio files\n2. Upload to a GitHub repository and get the raw link\n3. Use a professional CDN service (Alibaba Cloud OSS, Tencent Cloud COS, etc.)\n\n\ud83d\udd27 GitHub direct link tutorial:\n1. Log in to GitHub, create a new repository\n2. Upload audio files to the repository\n3. Click on the file \u2192 Raw button\n4. Copy the raw link address\n\n\ud83d\udca1 Custom upload service:\nIf you have your own file upload API, please enter the custom upload service address above\n`;\n            \n            throw new Error(errorMessage);\n        }\n\n        \/\/ Generate UUID\n        function generateUUID() {\n            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(\/&#091;xy]\/g, function(c) {\n                const r = Math.random() * 16 | 0;\n                const v = c == 'x' ? r : (r &amp; 0x3 | 0x8);\n                return v.toString(16);\n            });\n        }\n\n        \/\/ Create audio URL\n        async function createAudioUrl(audioBlob) {\n            const customUploadUrl = customUploadUrlInput.value.trim();\n            \n            \/\/ If the user has provided a custom upload service, prioritize its use\n            if (customUploadUrl) {\n                try {\n                    showStatus('\ud83d\udce4 Using custom upload service...', 'info');\n                    console.log('Attempting custom upload service:', customUploadUrl);\n                    \n                    const formData = new FormData();\n                    formData.append('file', audioBlob);\n                    formData.append('audio', audioBlob); \/\/ Compatible with different field names\n                    \n                    const response = await fetch(customUploadUrl, {\n                        method: 'POST',\n                        body: formData\n                    });\n                    \n                    if (!response.ok) {\n                        throw new Error(`Custom service response failed: ${response.status}`);\n                    }\n                    \n                    const result = await response.json();\n                    const url = result.url || result.file_url || result.link;\n                    \n                    if (url) {\n                        console.log('Custom service upload successful:', url);\n                        showStatus('\u2705 Custom service upload successful!', 'success');\n                        return url;\n                    } else {\n                        throw new Error('Custom service returned data in incorrect format');\n                    }\n                    \n                } catch (error) {\n                    console.warn('Custom upload service failed, switching to free service:', error.message);\n                    showStatus('\u26a0\ufe0f Custom service failed, switching to free hosting service...', 'info');\n                }\n            }\n            \n            \/\/ Use free hosting service\n            return await uploadToCatbox(audioBlob);\n        }\n\n        \/\/ Convert audio to text\n        async function convertToText() {\n            const provider = apiProvider.value;\n            \n            \/\/ Check if a file is required\n            if (provider === 'siliconflow' || (provider === 'volcengine' &amp;&amp; audioUrlMethod.value === 'upload')) {\n                if (!currentAudioBlob) {\n                    showStatus('\u26a0\ufe0f Please select an audio file first', 'error');\n                    return;\n                }\n            }\n\n            console.log('Starting conversion, provider:', provider, 'File size:', currentAudioBlob ? currentAudioBlob.size : 'Using URL');\n            \n            try {\n                if (provider === 'siliconflow') {\n                    await convertWithSiliconFlow(currentAudioBlob);\n                } else if (provider === 'volcengine') {\n                    await convertWithVolcEngine(currentAudioBlob);\n                }\n            } catch (error) {\n                console.error('Conversion failed:', error);\n                showStatus(`\u274c Conversion failed: ${error.message}`, 'error');\n                displayErrorInfo(provider, error);\n            }\n        }\n\n        \/\/ SiliconFlow conversion (pure transcription)\n        async function convertWithSiliconFlow(audioBlob) {\n            const apiUrl = document.getElementById('apiUrl').value.trim();\n            const apiKey = document.getElementById('apiKey').value.trim();\n            const model = document.getElementById('model').value.trim();\n\n            if (!apiKey) {\n                throw new Error('Please enter the API key for SiliconFlow');\n            }\n\n            showStatus('\ud83d\udd04 Converting to text...', 'info');\n            \n            const formData = new FormData();\n            formData.append('file', audioBlob, 'recording.webm');\n            formData.append('model', model);\n\n            console.log('Sending SiliconFlow request:', { apiUrl, model, fileSize: audioBlob.size });\n\n            const response = await fetch(apiUrl, {\n                method: 'POST',\n                headers: { 'Authorization': `Bearer ${apiKey}` },\n                body: formData\n            });\n\n            console.log('SiliconFlow response status:', response.status);\n\n            if (!response.ok) {\n                let errorMessage = `API request failed: ${response.status} ${response.statusText}`;\n                try {\n                    const errorData = await response.json();\n                    console.log('SiliconFlow error response:', errorData);\n                    if (errorData.error &amp;&amp; errorData.error.message) {\n                        errorMessage += ` - ${errorData.error.message}`;\n                    }\n                } catch (e) {\n                    console.log('Unable to parse error response');\n                }\n                throw new Error(errorMessage);\n            }\n\n            const result = await response.json();\n            console.log('SiliconFlow successful response:', result);\n            \n            const transcription = result.text;\n\n            if (!transcription) {\n                throw new Error('Conversion result is empty');\n            }\n\n            \/\/ First, reset the interface\n            resetResults();\n            \n            \/\/ Then save and display the results\n            currentTranscription = transcription;\n            currentSpeakerSegments = &#091;];\n            resultText.textContent = transcription;\n            \n            \/\/ Enable the export button\n            exportBtn.style.display = 'inline-block';\n            \n            showStatus('\u2705 Conversion completed!', 'success');\n            console.log('SiliconFlow conversion completed, result length:', transcription.length);\n            console.log('Saved result:', currentTranscription.substring(0, 100) + '...');\n        }\n\n        \/\/ Submit task to Volcengine\n        async function submitVolcEngineTask(audioUrl) {\n            const appKey = document.getElementById('volcAppKey').value.trim();\n            const accessKey = document.getElementById('volcAccessKey').value.trim();\n            const enableSpeakerDetection = document.getElementById('enableSpeakerDetection').checked;\n            \n            if (!appKey || !accessKey) {\n                throw new Error('Please enter the APP ID and Access Token for Volcengine');\n            }\n            \n            const taskId = generateUUID();\n            \n            const requestBody = {\n                user: {\n                    uid: \"web-user-\" + Date.now()\n                },\n                audio: {\n                    format: \"mp3\",\n                    url: audioUrl\n                },\n                request: {\n                    model_name: \"bigmodel\",\n                    enable_itn: true,\n                    enable_punc: true,\n                    enable_speaker_info: enableSpeakerDetection,\n                    show_utterances: true\n                }\n            };\n            \n            console.log('Submitted request parameters:', JSON.stringify(requestBody, null, 2)); \/\/ Add request parameter log\n            console.log('Speaker detection enabled:', enableSpeakerDetection); \/\/ Clearly show parameter status\n            \n            const response = await fetch('https:\/\/openspeech.bytedance.com\/api\/v3\/auc\/bigmodel\/submit', {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application\/json',\n                    'X-Api-App-Key': appKey,\n                    'X-Api-Access-Key': accessKey,\n                    'X-Api-Resource-Id': 'volc.bigasr.auc',\n                    'X-Api-Request-Id': taskId,\n                    'X-Api-Sequence': '-1'\n                },\n                body: JSON.stringify(requestBody)\n            });\n            \n            console.log('Task submission response status:', response.status); \/\/ Add response status log\n            console.log('Task submission response headers:', Object.fromEntries(response.headers.entries())); \/\/ Add response header log\n            \n            if (!response.ok) {\n                const statusCode = response.headers.get('X-Api-Status-Code');\n                const message = response.headers.get('X-Api-Message');\n                throw new Error(`Task submission failed: ${statusCode} - ${message}`);\n            }\n            \n            return taskId;\n        }\n\n        \/\/ Query results from Volcengine\n        async function queryVolcEngineResult(taskId) {\n            const appKey = document.getElementById('volcAppKey').value.trim();\n            const accessKey = document.getElementById('volcAccessKey').value.trim();\n            \n            console.log('Query task ID:', taskId); \/\/ Add task ID log\n            \n            const response = await fetch('https:\/\/openspeech.bytedance.com\/api\/v3\/auc\/bigmodel\/query', {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application\/json',\n                    'X-Api-App-Key': appKey,\n                    'X-Api-Access-Key': accessKey,\n                    'X-Api-Resource-Id': 'volc.bigasr.auc',\n                    'X-Api-Request-Id': taskId,\n                    'X-Api-Sequence': '-1'\n                },\n                body: '{}'\n            });\n            \n            const statusCode = response.headers.get('X-Api-Status-Code');\n            console.log('Query response status code:', statusCode); \/\/ Add status code log\n            console.log('Query response headers:', Object.fromEntries(response.headers.entries())); \/\/ Add response header log\n            \n            if (statusCode === '20000000') {\n                \/\/ Success\n                const result = await response.json();\n                console.log('Query successful, full returned result:', JSON.stringify(result, null, 2)); \/\/ Add full result log\n                return { status: 'completed', data: result };\n            } else if (statusCode === '40000001') {\n                \/\/ Processing\n                console.log('Task is still processing...'); \/\/ Add processing log\n                return { status: 'processing' };\n            } else {\n                \/\/ Failure\n                const message = response.headers.get('X-Api-Message');\n                console.error('Query failed:', statusCode, message); \/\/ Add failure log\n                throw new Error(`Query failed: ${statusCode} - ${message}`);\n            }\n        }\n\n        \/\/ Poll results from Volcengine\n        async function pollVolcEngineResult(taskId, maxAttempts = 30) {\n            for (let i = 0; i &lt; maxAttempts; i++) {\n                try {\n                    const result = await queryVolcEngineResult(taskId);\n                    \n                    if (result.status === 'completed') {\n                        return result.data;\n                    } else if (result.status === 'processing') {\n                        showStatus(`\ud83d\udd04 Processing... (${i + 1}\/${maxAttempts})`, 'info');\n                        await new Promise(resolve =&gt; setTimeout(resolve, 2000)); \/\/ Wait for 2 seconds\n                        continue;\n                    }\n                } catch (error) {\n                    if (i === maxAttempts - 1) throw error;\n                    await new Promise(resolve =&gt; setTimeout(resolve, 2000));\n                }\n            }\n            \n            throw new Error('Processing timed out, please try again later');\n        }\n\n        \/\/ VolcEngine conversion\n        async function convertWithVolcEngine(audioBlob) {\n            showStatus('\ud83d\udd04 Preparing for speech recognition...', 'info');\n            \n            \/\/ Get audio URL\n            let audioUrl;\n            if (audioUrlMethod.value === 'direct') {\n                audioUrl = directAudioUrlInput.value.trim();\n                if (!audioUrl) {\n                    throw new Error('Please enter the audio file URL first');\n                }\n                if (!audioUrl.startsWith('http:\/\/') &amp;&amp; !audioUrl.startsWith('https:\/\/')) {\n                    throw new Error('Audio URL must start with http:\/\/ or https:\/\/');\n                }\n                showStatus(`\ud83d\udcc1 Using direct URL: ${audioUrl}`, 'info');\n                console.log('Using direct URL:', audioUrl);\n            } else {\n                showStatus('\ud83d\udd04 Uploading audio file...', 'info');\n                audioUrl = await createAudioUrl(audioBlob);\n                console.log('Audio upload completed, URL:', audioUrl);\n            }\n            \n            showStatus('\ud83d\udd04 Submitting recognition task...', 'info');\n            \n            \/\/ Submit task\n            const taskId = await submitVolcEngineTask(audioUrl);\n            console.log('Task submitted successfully, ID:', taskId);\n            \n            showStatus('\ud83d\udd04 Processing audio, please wait...', 'info');\n            \n            \/\/ Poll for results\n            const volcResult = await pollVolcEngineResult(taskId);\n            console.log('VolcEngine processing completed:', volcResult);\n            \n            \/\/ Parse result\n            const transcription = volcResult.result.text;\n            \n            if (!transcription) {\n                throw new Error('Conversion result is empty');\n            }\n\n            \/\/ Reset interface first\n            resetResults();\n\n            \/\/ Save basic results\n            currentTranscription = transcription;\n            currentSpeakerSegments = &#091;];\n\n            \/\/ Display basic results\n            resultText.textContent = transcription;\n            \n            \/\/ Enable export button\n            exportBtn.style.display = 'inline-block';\n            \n            \/\/ Check if speaker detection is enabled and has results\n            const enableSpeakerDetection = document.getElementById('enableSpeakerDetection').checked;\n            if (enableSpeakerDetection&amp;&amp; volcResult.result.utterances &amp;&amp; volcResult.result.utterances.length &gt; 0) {\n                const speakerSegments = parseVolcEngineSpeakerResult(volcResult);\n                console.log('Parsed speaker segments:', speakerSegments); \/\/ Add debug log\n                \n                if (speakerSegments.length &gt; 0) {\n                    \/\/ Save speaker separation results\n                    currentSpeakerSegments = speakerSegments;\n                    \n                    \/\/ Generate text format with speaker markers\n                    let speakerText = '';\n                    speakerSegments.forEach((segment) =&gt; {\n                        speakerText += `&#091;Speaker ${segment.speaker}] (${formatTime(segment.startTime)} - ${formatTime(segment.endTime)})\n`;\n                        speakerText += `${segment.text}\n\n`;\n                    });\n                    \n                    \/\/ Display text with speaker markers\n                    resultText.textContent = speakerText.trim();\n                    \n                    \/\/ Display analysis information\n                    if (volcResult.audio_info) {\n                        audioAnalysis.style.display = 'block';\n                        const urlMethod = audioUrlMethod.value === 'direct' ? 'Direct URL' : 'Free Hosting Service';\n                        const speakerCount = Math.max(...speakerSegments.map(s =&gt; s.speaker));\n                        analysisInfo.innerHTML = `\n                            &lt;p&gt;&lt;strong&gt;Audio Duration:&lt;\/strong&gt; ${(volcResult.audio_info.duration \/ 1000).toFixed(2)} seconds&lt;\/p&gt;\n                            &lt;p&gt;&lt;strong&gt;Number of Detected Speakers:&lt;\/strong&gt; ${speakerCount} people&lt;\/p&gt;\n                            &lt;p&gt;&lt;strong&gt;Number of Audio Segments:&lt;\/strong&gt; ${speakerSegments.length} segments&lt;\/p&gt;\n                            &lt;p&gt;&lt;strong&gt;API Provider:&lt;\/strong&gt; Volcano Engine (Professional Level)&lt;\/p&gt;\n                            &lt;p&gt;&lt;strong&gt;Audio Retrieval Method:&lt;\/strong&gt; ${urlMethod}&lt;<\/p>\n                        `;\n                    }\n                    \n                    showStatus(`\u2705 Conversion complete! Detected ${Math.max(...speakerSegments.map(s =&gt; s.speaker))} speaker(s)`, 'success');\n                } else {\n                    \/\/ No speaker separation results, display full text\n                    resultText.textContent = transcription;\n                    showStatus('\u2705 Conversion complete! No multiple speakers detected', 'success');\n                }\n            } else {\n                \/\/ Speaker separation not enabled or no utterances, display full text\n                resultText.textContent = transcription;\n                showStatus('\u2705 Conversion complete!', 'success');\n            }\n            \n            console.log('Volc Engine conversion complete, result length:', transcription.length);\n            console.log('Saved result:', currentTranscription.substring(0, 100) + '...');\n        }\n\n        \/\/ Parse Volc Engine speaker results\n        function parseVolcEngineSpeakerResult(volcResult) {\n            console.log('Complete Volc Engine return result:', volcResult); \/\/ Add full debug log\n            \n            if (!volcResult.result || !volcResult.result.utterances) {\n                console.log('No utterances data found');\n                return &#091;];\n            }\n            \n            const utterances = volcResult.result.utterances;\n            console.log('utterances data:', utterances); \/\/ Add utterances debug log\n            console.log('utterances array length:', utterances.length);\n            const segments =&#091;];\n            \n            utterances.forEach((utterance, index) =&gt; {\n                console.log(`utterance ${index} complete structure:`, JSON.stringify(utterance, null, 2)); \/\/ Add detailed utterance structure log\n                \n                \/\/ Try multiple possible speaker ID fields\n                let speakerId = 1; \/\/ Default value\n                \n                \/\/ Try different field names by priority\n                if (utterance.additions&amp;&amp;if (utterance.additions.speaker !== undefined) {\n                    speakerId = parseInt(utterance.additions.speaker); \/\/ The speaker ID from Volcano Engine is in additions.speaker\n                    console.log(`Found additions.speaker: ${speakerId}`);\n                } else if (utterance.speaker_id !== undefined) {\n                    speakerId = utterance.speaker_id;\n                    console.log(`Found speaker_id: ${speakerId}`);\n                } else if (utterance.spk_id !== undefined) {\n                    speakerId = utterance.spk_id;\n                    console.log(`Found spk_id: ${speakerId}`);\n                } else if (utterance.speaker !== undefined) {\n                    speakerId = utterance.speaker;\n                    console.log(`Found speaker: ${speakerId}`);\n                } else if (utterance.channel_id !== undefined) {\n                    speakerId = utterance.channel_id;\n                    console.log(`Found channel_id: ${speakerId}`);\n                } else if (utterance.spk !== undefined) {\n                    speakerId = utterance.spk;\n                    console.log(`Found spk: ${speakerId}`);\n                } else {\n                    console.log(`Utterance ${index} has no speaker ID field found, using default value 1`);\n                }\n                \n                segments.push({\n                    speaker: speakerId,\n                    startTime: utterance.start_time \/ 1000, \/\/ Convert to seconds\n                    endTime: utterance.end_time \/ 1000,\n                    text: utterance.text\n                });\n                \n                console.log(`Parse result ${index}: speaker=${speakerId}, text=\"${utterance.text.substring(0, 20)}...\"`);\n            });\n            \n            console.log('Parsed segments:', segments); \/\/ Add parsed result debug log\n            \n            \/\/ Count the number of speakers\n            const speakers = &#091;...new Set(segments.map(s =&gt; s.speaker))];\n            console.log('Detected speaker IDs:', speakers);\n            console.log('Number of speakers:', speakers.length);\n            \n            return segments;\n        }\n\n        \/\/ Display Volcano Engine speaker results\n        function displayVolcEngineSpeakerResults(segments) {\n            \/\/ This function is no longer needed as we now display text with speaker markers directly in resultText\n            console.log('displayVolcEngineSpeakerResults function is deprecated');\n        }\n\n        \/\/ Format time\n        function formatTime(seconds) {\n            const mins = Math.floor(seconds \/ 60);\n            const secs = Math.floor(seconds % 60);\n            return `${mins}:${secs.toString().padStart(2, '0')}`;\n        }\n\n        \/\/ Toggle view mode (deprecated)\n        function toggleViewMode() {\n            \/\/ This function is no longer needed as we have removed the view switching functionality\n            console.log('toggleViewMode function is deprecated');\n        }\n\n        \/\/ Display error information\n        function displayErrorInfo(provider, error) {\n            if (provider === 'siliconflow') {\n                resultText.innerHTML = `Conversion failed. Please check:\n1. Is the API key correct?\n2. Is the network connection normal?\n3. Is the audio file format supported?\n\nError details: ${error.message}`;\n            } else if (provider === 'volcengine') {\n                \/\/ Check if it's an audio download failure error\n                if (error.message.includes('audio download failed') || error.message.includes('Invalid audio URI')) {\n                    resultText.innerHTML = `\n&lt;div style=\"color: #e74c3c; margin-bottom: 15px;\"&gt;\n&lt;strong&gt;\u274c Audio file download failed&lt;\/strong&gt;\n&lt;\/div&gt;\n\n&lt;div style=\"background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin-bottom: 15px;\"&gt;\n&lt;strong&gt;\ud83d\udd0d Problem cause:&lt;\/strong&gt;&lt;br&gt;\nThe URL you provided is not a valid direct link to an audio file. The Volcengine API cannot download the file directly.&lt;\/div&gt;\n\n&lt;div style=\"background: #d1ecf1; padding: 15px; border-radius: 8px; border-left: 4px solid #bee5eb; margin-bottom: 15px;\"&gt;\n&lt;strong&gt;\u2705 Solution:&lt;\/strong&gt;&lt;br&gt;\n&lt;br&gt;\n&lt;strong&gt;1. Check if the URL is a direct link:&lt;\/strong&gt;&lt;br&gt;\n\u2022 Open your URL directly in a browser&lt;br&gt;\n\u2022 If it immediately starts downloading\/playing the audio file, it's a valid direct link&lt;br&gt;\n\u2022 If it redirects to a login or sharing page, it's an invalid link&lt;br&gt;\n&lt;br&gt;\n&lt;strong&gt;2. How to obtain a valid direct link:&lt;\/strong&gt;&lt;br&gt;\n\u2022 &lt;strong&gt;GitHub method:&lt;\/strong&gt;Upload to a GitHub repository, click on the file \u2192 Raw, and copy the link&lt;br&gt;\n\u2022 &lt;strong&gt;Self-owned server:&lt;\/strong&gt;Upload to your website server&lt;br&gt;\n\u2022 &lt;strong&gt;Professional CDN:&lt;\/strong&gt;Use services like Alibaba Cloud OSS, Tencent Cloud COS, etc.&lt;br&gt;\n\u2022 &lt;strong&gt;Free hosting:&lt;Switch to \"Obtain URL via Upload Service\" mode&lt;br&gt;\n&lt;\/div&gt;\n\n&lt;div style=\"background: #f8d7da; padding: 10px; border-radius: 8px; border-left: 4px solid #dc3545;\"&gt;\n&lt;\u26a0\ufe0f The following link types are invalid:&lt;\/strong&gt;&lt;\n\u2022 Baidu Netdisk, OneDrive, Google Drive share links&lt;\n\u2022 Links requiring login verification&lt;\n\u2022 Redirect links&lt;br&gt;\n&lt;\/div&gt;\n\n&lt;Error details:&lt;\/strong&gt; ${error.message}`;\n                } else {\n                    resultText.innerHTML = `Conversion failed. Please check:\n1. Whether the Volcano Engine APP ID and Access Token are correct\n2. Whether the network connection is normal\n3. Whether the audio file URL is a valid direct link\n\nError details: ${error.message}`;\n                }\n            }\n        }\n\n        \/\/ Validate if the URL is a valid direct link\n        function validateUrl() {\n            const url = directAudioUrlInput.value.trim();\n            const resultDiv = urlValidationResult;\n            resultDiv.style.display = 'none'; \/\/ Hide previous results\n\n            if (!url) {\n                resultDiv.textContent = 'Please enter a URL first';\n                resultDiv.style.display = 'block';\n                return;\n            }\n\n            if (url.startsWith('http:\/\/') || url.startsWith('https:\/\/')) {\n                resultDiv.textContent = '\u2705 This is a valid direct link URL!';\n                resultDiv.style.color = 'green';\n                resultDiv.style.fontWeight = 'bold';\n                resultDiv.style.display = 'block';\n            } else {\n                resultDiv.textContent = '\u274c This is not a valid direct link URL. Please ensure it starts with http:\/\/ or https:\/\/.';\n                resultDiv.style.color = 'red';\n                resultDiv.style.fontWeight = 'bold';\n                resultDiv.style.display = 'block';\n            }\n        }\n\n        \/\/ Handle file upload\n        audioFileInput.addEventListener('change', async (event) =&gt; {\n            const file = event.target.files&#091;0];\n            if (file) {\n                console.log('Selected file:', file.name, file.type, file.size);\n                \n                \/\/ Check file type\n                if (!file.type.startsWith('audio\/')) {\n                    showStatus('\u274c Please select an audio file', 'error');\n                    return;\n                }\n\n                \/\/ Check file size (limit to 50MB)\n                if (file.size &gt; 50 * 1024 * 1024) {\n                    showStatus('\u274c File is too large, please select an audio file smaller than 50MB', 'error');\n                    return;\n                }\n\n                currentAudioBlob = file;\n                \n                \/\/ Display audio player\n                const audioUrl = URL.createObjectURL(file);\n                audioPlayer.src = audioUrl;\n                audioPlayer.style.display = 'block';\n                \n                \/\/ Display start conversion button\n                updateConvertButtonState(); \/\/ Update button state\n            }\n        });\n\n        \/\/ Event listeners\n        convertBtn.addEventListener('click', () =&gt; {\n            console.log('Convert button clicked');\n            convertToText();\n        });\n        \n        exportBtn.addEventListener('click', () =&gt; {\n            console.log('Export button clicked');\n            exportToTxt();\n        });\n        \n        \/\/ toggleView.addEventListener('click', toggleViewMode); \/\/ Remove view switch button listener\n        apiProvider.addEventListener('change', switchApiProvider);\n        audioUrlMethod.addEventListener('change', switchAudioUrlMethod);\n        directAudioUrlInput.addEventListener('input', updateConvertButtonState); \/\/ Add URL input listener\n        validateUrlBtn.addEventListener('click', validateUrl); \/\/ Add URL validation button listener\n\n        \/\/ Initialization after page load\n        document.addEventListener('DOMContentLoaded', () =&gt; {\n            console.log('Page loaded');\n            showStatus('\ud83d\udd27 Please select an API provider and configure the relevant information.', 'info');\n            \n            \/\/ Initialize view\n            \/\/ toggleView.style.display = 'none'; \/\/ Remove view switch button\n            convertBtn.style.display = 'none';\n            exportBtn.style.display = 'none';\n            switchApiProvider(); \/\/ Initialize API configuration display\n            \n            \/\/ Add listeners to selected input fields to update button state in real time\n            document.getElementById('apiKey').addEventListener('input', updateConvertButtonState);\n            document.getElementById('volcAppKey').addEventListener('input', updateConvertButtonState);\n            document.getElementById('volcAccessKey').addEventListener('input', updateConvertButtonState);\n        });\n    &lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>A web-based tool for converting audio recordings into text using speech recognition APIs.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24],"tags":[16,13],"class_list":["post-57","post","type-post","status-publish","format-standard","hentry","category-articles","tag-api","tag-13"],"_links":{"self":[{"href":"https:\/\/en.blog.liu-qi.cn\/index.php\/wp-json\/wp\/v2\/posts\/57","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/en.blog.liu-qi.cn\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/en.blog.liu-qi.cn\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/en.blog.liu-qi.cn\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/en.blog.liu-qi.cn\/index.php\/wp-json\/wp\/v2\/comments?post=57"}],"version-history":[{"count":0,"href":"https:\/\/en.blog.liu-qi.cn\/index.php\/wp-json\/wp\/v2\/posts\/57\/revisions"}],"wp:attachment":[{"href":"https:\/\/en.blog.liu-qi.cn\/index.php\/wp-json\/wp\/v2\/media?parent=57"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/en.blog.liu-qi.cn\/index.php\/wp-json\/wp\/v2\/categories?post=57"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/en.blog.liu-qi.cn\/index.php\/wp-json\/wp\/v2\/tags?post=57"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}