import { Asr, type AsrRequest, type AsrResponse } from './auc.ts';
import { getConfig, setConfig } from './config.ts';
import { AudioRecorder, RecordingState, type RecorderOptions } from './recorder.ts';
// 创建页面结构
function createPageStructure() {
const html = `
`;
document.body.innerHTML = html;
}
// 全局变量存储选中的文件
let selectedFile: File | null = null;
// 全局变量存储录制器和录制状态
let audioRecorder: AudioRecorder | null = null;
let recordedAudioBlob: Blob | null = null;
let isUsingRecordedAudio = false;
// 文件上传处理函数
function setupFileUpload() {
const fileInput = document.getElementById('audioFile') as HTMLInputElement;
const uploadBtn = document.getElementById('uploadBtn') as HTMLButtonElement;
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement;
const fileInfo = document.getElementById('fileInfo') as HTMLDivElement;
// 点击上传按钮,触发文件选择
uploadBtn.addEventListener('click', () => {
fileInput.click();
});
// 文件选择处理
fileInput.addEventListener('change', (event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
// 验证文件类型
if (!file.type.startsWith('audio/')) {
alert('请选择音频文件!');
return;
}
// 验证文件大小 (例如限制为100MB)
const maxSize = 100 * 1024 * 1024; // 100MB
if (file.size > maxSize) {
alert('文件大小不能超过100MB!');
return;
}
selectedFile = file;
// 清除录制状态
recordedAudioBlob = null;
isUsingRecordedAudio = false;
// 显示文件信息
const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
fileInfo.innerHTML = `
已选择文件: ${file.name}
文件大小: ${fileSizeMB} MB
文件类型: ${file.type}
`;
// 启用转换按钮
convertBtn.disabled = false;
uploadBtn.textContent = '重新选择文件';
}
});
}
// 将文件转换为base64
function fileToBase64(file: File): Promise {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
// 移除data:audio/xxx;base64,前缀,只保留base64数据
const base64Data = result.split(',')[1];
resolve(base64Data);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// 获取音频格式
function getAudioFormat(file: File): string {
const mimeType = file.type;
if (mimeType.includes('wav')) return 'wav';
if (mimeType.includes('mp3')) return 'mp3';
if (mimeType.includes('ogg')) return 'ogg';
if (mimeType.includes('pcm')) return 'pcm';
if (mimeType.includes('webm')) return 'webm';
// 默认返回wav
return 'wav';
}
// 从Blob获取音频格式
function getAudioFormatFromBlob(blob: Blob): string {
const mimeType = blob.type;
if (mimeType.includes('wav')) return 'wav';
if (mimeType.includes('mp3')) return 'mp3';
if (mimeType.includes('ogg')) return 'ogg';
if (mimeType.includes('pcm')) return 'pcm';
if (mimeType.includes('webm')) return 'webm';
// 默认返回webm (录制的音频通常是webm格式)
return 'webm';
}
// ASR转换功能
async function convertToText() {
if (!selectedFile && !recordedAudioBlob) {
alert('请先选择音频文件或录制音频!');
return;
}
const loadingEl = document.getElementById('loading') as HTMLDivElement;
const resultEl = document.getElementById('result') as HTMLDivElement;
const resultText = document.getElementById('resultText') as HTMLDivElement;
const resultDetails = document.getElementById('resultDetails') as HTMLDivElement;
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement;
try {
// 显示加载状态
loadingEl.style.display = 'flex';
resultEl.style.display = 'none';
convertBtn.disabled = true;
convertBtn.textContent = '转换中...';
// 获取音频数据
let base64Data: string;
let audioFormat: string;
if (isUsingRecordedAudio && recordedAudioBlob) {
// 使用录制的音频
const recordedFile = AudioRecorder.blobToFile(recordedAudioBlob, 'recording.webm');
base64Data = await fileToBase64(recordedFile);
audioFormat = getAudioFormatFromBlob(recordedAudioBlob);
} else if (selectedFile) {
// 使用选择的文件
base64Data = await fileToBase64(selectedFile);
audioFormat = getAudioFormat(selectedFile);
} else {
throw new Error('没有可用的音频数据');
}
const config = getConfig();
const VOLCENGINE_AUC_APPID = config.VOLCENGINE_AUC_APPID;
const VOLCENGINE_AUC_TOKEN = config.VOLCENGINE_AUC_TOKEN;
// 配置ASR请求
// 注意:这里需要提供真实的API密钥
const asr = new Asr({
appid: VOLCENGINE_AUC_APPID, // 请替换为真实的APP ID
token: VOLCENGINE_AUC_TOKEN, // 请替换为真实的ACCESS TOKEN
type: 'flash' // 使用flash模式进行快速识别
});
const asrRequest: AsrRequest = {
audio: {
data: base64Data,
format: audioFormat as any,
rate: 16000,
channel: 1
},
request: {
enable_words: true,
enable_sentence_info: true,
enable_utterance_info: true,
enable_punctuation_prediction: true,
enable_inverse_text_normalization: true
}
};
// 调用ASR API
const response: AsrResponse = await asr.getText(asrRequest);
// 隐藏加载状态
loadingEl.style.display = 'none';
// 显示结果
if (response.result && response.result.text) {
resultText.innerHTML = `${response.result.text}
`;
// 显示详细信息
const duration = response.audio_info?.duration ? (response.audio_info.duration / 1000).toFixed(2) + '秒' : '未知';
let detailsHtml = `
`;
// 如果有语句级别的信息,显示时间戳
if (response.result.utterances && response.result.utterances.length > 0) {
detailsHtml += `
分句结果:
`;
response.result.utterances.forEach((utterance, index) => {
const startTime = (utterance.start_time / 1000).toFixed(2);
const endTime = (utterance.end_time / 1000).toFixed(2);
detailsHtml += `
-
${startTime}s - ${endTime}s:
${utterance.text}
`;
});
detailsHtml += `
`;
}
resultDetails.innerHTML = detailsHtml;
resultEl.style.display = 'block';
} else {
throw new Error('转换失败:未能获取到文字结果');
}
} catch (error) {
console.error('转换出错:', error);
loadingEl.style.display = 'none';
let errorMessage = '转换失败,请稍后重试。';
if (error instanceof Error) {
if (error.message.includes('VOLCENGINE_Asr_APPID') || error.message.includes('VOLCENGINE_Asr_TOKEN')) {
errorMessage = '请配置正确的API密钥(APP ID和Access Token)后重试。';
} else {
errorMessage = `转换失败: ${error.message}`;
}
}
alert(errorMessage);
} finally {
convertBtn.disabled = false;
convertBtn.textContent = '转换为文字';
}
}
// 设置转换按钮
function setupConvertButton() {
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement;
convertBtn.addEventListener('click', convertToText);
}
// 设置弹窗功能
function setupSettingsModal() {
const settingsBtn = document.getElementById('settingsBtn') as HTMLButtonElement;
const settingsModal = document.getElementById('settingsModal') as HTMLDivElement;
const closeModal = document.getElementById('closeModal') as HTMLButtonElement;
const saveSettings = document.getElementById('saveSettings') as HTMLButtonElement;
const cancelSettings = document.getElementById('cancelSettings') as HTMLButtonElement;
const appIdInput = document.getElementById('appId') as HTMLInputElement;
const accessTokenInput = document.getElementById('accessToken') as HTMLInputElement;
// 打开设置弹窗
function openSettingsModal() {
// 加载当前配置
const config = getConfig();
appIdInput.value = config.VOLCENGINE_AUC_APPID;
accessTokenInput.value = config.VOLCENGINE_AUC_TOKEN;
settingsModal.style.display = 'flex';
document.body.style.overflow = 'hidden'; // 防止背景滚动
}
// 关闭设置弹窗
function closeSettingsModal() {
settingsModal.style.display = 'none';
document.body.style.overflow = 'auto'; // 恢复滚动
}
// 保存设置
function saveSettingsHandler() {
const appId = appIdInput.value.trim();
const accessToken = accessTokenInput.value.trim();
if (!appId || !accessToken) {
alert('请填写完整的APP ID和Access Token');
return;
}
// 保存到localStorage
try {
setConfig({
VOLCENGINE_AUC_APPID: appId,
VOLCENGINE_AUC_TOKEN: accessToken
});
alert('设置保存成功!');
closeSettingsModal();
} catch (error) {
console.error('保存设置失败:', error);
alert('保存设置失败,请重试');
}
}
// 绑定事件
settingsBtn.addEventListener('click', openSettingsModal);
closeModal.addEventListener('click', closeSettingsModal);
cancelSettings.addEventListener('click', closeSettingsModal);
saveSettings.addEventListener('click', saveSettingsHandler);
// 点击弹窗背景关闭
settingsModal.addEventListener('click', (e) => {
if (e.target === settingsModal) {
closeSettingsModal();
}
});
// ESC键关闭弹窗
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && settingsModal.style.display === 'flex') {
closeSettingsModal();
}
});
}
// 录制功能设置
function setupRecording() {
const recordBtn = document.getElementById('recordBtn') as HTMLButtonElement;
const recordIcon = document.getElementById('recordIcon') as HTMLElement;
const recordText = document.getElementById('recordText') as HTMLSpanElement;
const recordStatus = document.getElementById('recordStatus') as HTMLDivElement;
const recordTime = document.getElementById('recordTime') as HTMLSpanElement;
const fileInfo = document.getElementById('fileInfo') as HTMLDivElement;
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement;
const uploadBtn = document.getElementById('uploadBtn') as HTMLButtonElement;
// 检查浏览器支持
if (!AudioRecorder.isSupported()) {
recordBtn.disabled = true;
recordBtn.title = '当前浏览器不支持录音功能';
recordText.textContent = '不支持录音';
return;
}
// 初始化录制器
function initRecorder() {
if (audioRecorder) {
audioRecorder.destroy();
}
const options: RecorderOptions = {
audioBitsPerSecond: 128000,
mimeType: AudioRecorder.getSupportedMimeTypes()[0] || 'audio/webm'
};
audioRecorder = new AudioRecorder(options);
// 监听状态变化
audioRecorder.on('stateChange', (state: RecordingState) => {
updateRecordUI(state);
});
// 监听时间更新
audioRecorder.on('timeUpdate', (time: number) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
recordTime.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
});
// 监听录制完成
audioRecorder.on('dataAvailable', (blob: Blob) => {
recordedAudioBlob = blob;
isUsingRecordedAudio = true;
// 清除选择的文件
selectedFile = null;
const fileInput = document.getElementById('audioFile') as HTMLInputElement;
fileInput.value = '';
// 显示录制信息
const fileSizeMB = (blob.size / 1024 / 1024).toFixed(2);
fileInfo.innerHTML = `
录制完成: 新录音
录制时长: ${recordTime.textContent}
文件大小: ${fileSizeMB} MB
文件类型: ${blob.type}
`;
// 启用转换按钮
convertBtn.disabled = false;
// 更新上传按钮文字
uploadBtn.textContent = '选择音频文件';
});
// 监听错误
audioRecorder.on('error', (error: Error) => {
console.error('录制错误:', error);
alert(`录制失败: ${error.message}`);
updateRecordUI(RecordingState.IDLE);
});
}
// 更新录制UI
function updateRecordUI(state: RecordingState) {
switch (state) {
case RecordingState.IDLE:
recordBtn.classList.remove('recording', 'stopping');
recordBtn.disabled = false;
recordIcon.innerHTML = '';
recordText.textContent = '录制音频';
recordStatus.style.display = 'none';
recordTime.textContent = '00:00';
break;
case RecordingState.RECORDING:
recordBtn.classList.add('recording');
recordBtn.classList.remove('stopping');
recordBtn.disabled = false;
recordIcon.innerHTML = '';
recordText.textContent = '停止录制';
recordStatus.style.display = 'flex';
break;
case RecordingState.STOPPED:
recordBtn.classList.remove('recording');
recordBtn.classList.add('stopping');
recordBtn.disabled = true;
recordText.textContent = '处理中...';
recordStatus.style.display = 'none';
break;
}
}
// 录制按钮点击事件
recordBtn.addEventListener('click', async () => {
if (!audioRecorder) {
initRecorder();
}
try {
const currentState = audioRecorder!.getState();
if (currentState === RecordingState.IDLE) {
// 开始录制
await audioRecorder!.startRecording();
} else if (currentState === RecordingState.RECORDING) {
// 停止录制
audioRecorder!.stopRecording();
}
} catch (error) {
console.error('录制操作失败:', error);
alert(`操作失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
});
// 初始化录制器
initRecorder();
}
// 初始化页面
createPageStructure();
setupFileUpload();
setupConvertButton();
setupSettingsModal();
setupRecording();