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