6ee6fd64dd
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev. Co-authored-by: Cursor <cursoragent@cursor.com>
91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
const TARGET_SAMPLE_RATE = 16_000;
|
|
|
|
function mixToMono(buffer: AudioBuffer): AudioBuffer {
|
|
if (buffer.numberOfChannels === 1) return buffer;
|
|
const mono = new AudioBuffer({
|
|
length: buffer.length,
|
|
numberOfChannels: 1,
|
|
sampleRate: buffer.sampleRate,
|
|
});
|
|
const out = mono.getChannelData(0);
|
|
const channels = buffer.numberOfChannels;
|
|
for (let ch = 0; ch < channels; ch += 1) {
|
|
const data = buffer.getChannelData(ch);
|
|
for (let i = 0; i < buffer.length; i += 1) {
|
|
out[i] += data[i] / channels;
|
|
}
|
|
}
|
|
return mono;
|
|
}
|
|
|
|
function encodeWav(audioBuffer: AudioBuffer): Blob {
|
|
const channel = audioBuffer.getChannelData(0);
|
|
const dataSize = channel.length * 2;
|
|
const buffer = new ArrayBuffer(44 + dataSize);
|
|
const view = new DataView(buffer);
|
|
|
|
const writeString = (offset: number, value: string) => {
|
|
for (let i = 0; i < value.length; i += 1) {
|
|
view.setUint8(offset + i, value.charCodeAt(i));
|
|
}
|
|
};
|
|
|
|
writeString(0, 'RIFF');
|
|
view.setUint32(4, 36 + dataSize, true);
|
|
writeString(8, 'WAVE');
|
|
writeString(12, 'fmt ');
|
|
view.setUint32(16, 16, true);
|
|
view.setUint16(20, 1, true);
|
|
view.setUint16(22, 1, true);
|
|
view.setUint32(24, audioBuffer.sampleRate, true);
|
|
view.setUint32(28, audioBuffer.sampleRate * 2, true);
|
|
view.setUint16(32, 2, true);
|
|
view.setUint16(34, 16, true);
|
|
writeString(36, 'data');
|
|
view.setUint32(40, dataSize, true);
|
|
|
|
let offset = 44;
|
|
for (let i = 0; i < channel.length; i += 1) {
|
|
const sample = Math.max(-1, Math.min(1, channel[i] ?? 0));
|
|
view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
|
|
offset += 2;
|
|
}
|
|
|
|
return new Blob([buffer], { type: 'audio/wav' });
|
|
}
|
|
|
|
/** 将浏览器录音转为 16kHz 单声道 WAV,提升 ASR 解码成功率。失败时返回原 blob。 */
|
|
export async function blobToWav16kMono(blob: Blob): Promise<Blob> {
|
|
if (!blob.size) return blob;
|
|
|
|
const audioContext = new AudioContext();
|
|
try {
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
let decoded: AudioBuffer;
|
|
try {
|
|
decoded = await audioContext.decodeAudioData(arrayBuffer.slice(0));
|
|
} catch {
|
|
return blob;
|
|
}
|
|
|
|
const mono = mixToMono(decoded);
|
|
const duration = mono.duration;
|
|
if (!Number.isFinite(duration) || duration <= 0) return blob;
|
|
|
|
const offline = new OfflineAudioContext(
|
|
1,
|
|
Math.max(1, Math.ceil(duration * TARGET_SAMPLE_RATE)),
|
|
TARGET_SAMPLE_RATE,
|
|
);
|
|
const source = offline.createBufferSource();
|
|
source.buffer = mono;
|
|
source.connect(offline.destination);
|
|
source.start(0);
|
|
const rendered = await offline.startRendering();
|
|
if (!rendered.length) return blob;
|
|
return encodeWav(rendered);
|
|
} finally {
|
|
await audioContext.close().catch(() => undefined);
|
|
}
|
|
}
|