ui: Restructure repo to use tools/ui folder and ui / UI / llama-ui / LLAMA_UI naming (#23064)
* webui: Move static build output from `tools/server/public` to `build/ui` directory * refactor: Move to `tools/ui` * refactor: rename CMake variables and preprocessor defines - Rename LLAMA_BUILD_WEBUI -> LLAMA_BUILD_UI (old kept as deprecated) - Rename LLAMA_USE_PREBUILT_WEBUI -> LLAMA_USE_PREBUILT_UI (old kept as deprecated) - Backward compat: old vars auto-forward to new ones with DEPRECATION warning - Rename internal vars: WEBUI_SOURCE -> UI_SOURCE, WEBUI_SOURCE_DIR -> UI_SOURCE_DIR, etc. - Rename HF bucket: LLAMA_WEBUI_HF_BUCKET -> LLAMA_UI_HF_BUCKET - Emit both LLAMA_BUILD_WEBUI and LLAMA_BUILD_UI preprocessor defines - Emit both LLAMA_WEBUI_DEFAULT_ENABLED and LLAMA_UI_DEFAULT_ENABLED * refactor: rename CLI flags (--webui -> --ui) with backward compat - Add --ui/--no-ui (old --webui/--no-webui kept as deprecated aliases) - Add --ui-config (old --webui-config kept as deprecated alias) - Add --ui-config-file (old --webui-config-file kept as deprecated alias) - Add --ui-mcp-proxy/--no-ui-mcp-proxy (old --webui-mcp-proxy kept as deprecated) - Add new env vars: LLAMA_ARG_UI, LLAMA_ARG_UI_CONFIG, LLAMA_ARG_UI_CONFIG_FILE, LLAMA_ARG_UI_MCP_PROXY - C++ struct fields: params.ui, params.ui_config_json, params.ui_mcp_proxy added alongside old fields - Backward compat: old fields synced to new ones in g_params_to_internals * refactor: update C++ server internals with backward compat - Rename json_webui_settings -> json_ui_settings (both kept in server_context_meta) - Rename params.webui usage -> params.ui (both synced, old still works) - JSON API emits both "ui"/"ui_settings" and "webui"/"webui_settings" keys - Server routes use params.ui_mcp_proxy || params.webui_mcp_proxy - Preprocessor guards use #if defined(LLAMA_BUILD_UI) || defined(LLAMA_BUILD_WEBUI) * refactor: rename CI/CD workflows, artifacts, and build script - Rename webui-build.yml -> ui-build.yml; artifact webui-build -> ui-build - Rename webui-publish.yml -> ui-publish.yml; var HF_BUCKET_WEBUI_STATIC_OUTPUT -> HF_BUCKET_UI_STATIC_OUTPUT - Rename server-webui.yml -> server-ui.yml; job webui-build/checks -> ui-build/checks - Update server.yml: job/artifact refs webui-build -> ui-build - Update release.yml: all webui-build/publish refs -> ui-build/publish; HF_TOKEN_WEBUI_STATIC_OUTPUT -> HF_TOKEN_UI_STATIC_OUTPUT - Update server-self-hosted.yml: webui-build -> ui-build - Update build-self-hosted.yml: HF_WEBUI_VERSION -> HF_UI_VERSION - Rename webui-download.cmake -> ui-download.cmake (internal refs updated) - Update labeler.yml: server/webui -> server/ui path label * docs: update CODEOWNERS and server README docs - Update CODEOWNERS: team ggml-org/llama-webui -> ggml-org/llama-ui, path /tools/server/webui/ -> /tools/ui/ - Update server README.md: CLI tables show --ui flags with deprecated --webui aliases - Update server README-dev.md: "WebUI" -> "UI", paths updated to tools/ui/ * fix: Small fixes for UI build * fix: CMake.txt syntax * chore: Formatting * fix: `.editorconfig` for llama-ui * chore: Formatting * refactor: Use `APP_NAME` in Error route * refactor: Cleanup * refactor: Single migration service * make llama-ui a linkable target * fix: UI Build output * fix: Missing change * fix: separate llama-ui npm build output into build/tools/ui/dist subfolder + use cmake npm build instead of downloading ui-build.yml artifacts in CI * refactor: UI workflows cleanup --------- Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
This commit is contained in:
co-authored by
Xuan Son Nguyen
parent
49d1701bd2
commit
59778f0196
@@ -0,0 +1,257 @@
|
||||
import { MimeTypeAudio } from '$lib/enums';
|
||||
|
||||
/**
|
||||
* AudioRecorder - Browser-based audio recording with MediaRecorder API
|
||||
*
|
||||
* This class provides a complete audio recording solution using the browser's MediaRecorder API.
|
||||
* It handles microphone access, recording state management, and audio format optimization.
|
||||
*
|
||||
* **Features:**
|
||||
* - Automatic microphone permission handling
|
||||
* - Audio enhancement (echo cancellation, noise suppression, auto gain)
|
||||
* - Multiple format support with fallback (WAV, WebM, MP4, AAC)
|
||||
* - Real-time recording state tracking
|
||||
* - Proper cleanup and resource management
|
||||
*/
|
||||
export class AudioRecorder {
|
||||
private mediaRecorder: MediaRecorder | null = null;
|
||||
private audioChunks: Blob[] = [];
|
||||
private stream: MediaStream | null = null;
|
||||
private recordingState: boolean = false;
|
||||
|
||||
async startRecording(): Promise<void> {
|
||||
try {
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
}
|
||||
});
|
||||
|
||||
this.initializeRecorder(this.stream);
|
||||
|
||||
this.audioChunks = [];
|
||||
// Start recording with a small timeslice to ensure we get data
|
||||
this.mediaRecorder!.start(100);
|
||||
this.recordingState = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to start recording:', error);
|
||||
throw new Error('Failed to access microphone. Please check permissions.');
|
||||
}
|
||||
}
|
||||
|
||||
async stopRecording(): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const recorder = this.mediaRecorder;
|
||||
const chunks = this.audioChunks;
|
||||
const stream = this.stream;
|
||||
|
||||
if (!recorder || recorder.state === 'inactive') {
|
||||
reject(new Error('No active recording to stop'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Detach instance state right away so a new startRecording can take over without race
|
||||
this.mediaRecorder = null;
|
||||
this.audioChunks = [];
|
||||
this.stream = null;
|
||||
this.recordingState = false;
|
||||
|
||||
recorder.onstop = () => {
|
||||
const audioBlob = new Blob(chunks, {
|
||||
type: recorder.mimeType || MimeTypeAudio.WAV
|
||||
});
|
||||
|
||||
if (stream) {
|
||||
for (const track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
|
||||
resolve(audioBlob);
|
||||
};
|
||||
|
||||
recorder.onerror = (event) => {
|
||||
console.error('Recording error:', event);
|
||||
|
||||
if (stream) {
|
||||
for (const track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
|
||||
reject(new Error('Recording failed'));
|
||||
};
|
||||
|
||||
recorder.stop();
|
||||
});
|
||||
}
|
||||
|
||||
isRecording(): boolean {
|
||||
return this.recordingState;
|
||||
}
|
||||
|
||||
cancelRecording(): void {
|
||||
const recorder = this.mediaRecorder;
|
||||
const stream = this.stream;
|
||||
|
||||
this.mediaRecorder = null;
|
||||
this.audioChunks = [];
|
||||
this.stream = null;
|
||||
this.recordingState = false;
|
||||
|
||||
if (recorder && recorder.state !== 'inactive') {
|
||||
// Drop the original handlers so the pending stop event does not touch the instance
|
||||
recorder.onstop = null;
|
||||
recorder.onerror = null;
|
||||
recorder.stop();
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
for (const track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initializeRecorder(stream: MediaStream): void {
|
||||
const options: MediaRecorderOptions = {};
|
||||
|
||||
if (MediaRecorder.isTypeSupported(MimeTypeAudio.WAV)) {
|
||||
options.mimeType = MimeTypeAudio.WAV;
|
||||
} else if (MediaRecorder.isTypeSupported(MimeTypeAudio.WEBM_OPUS)) {
|
||||
options.mimeType = MimeTypeAudio.WEBM_OPUS;
|
||||
} else if (MediaRecorder.isTypeSupported(MimeTypeAudio.WEBM)) {
|
||||
options.mimeType = MimeTypeAudio.WEBM;
|
||||
} else if (MediaRecorder.isTypeSupported(MimeTypeAudio.MP4)) {
|
||||
options.mimeType = MimeTypeAudio.MP4;
|
||||
} else {
|
||||
console.warn('No preferred audio format supported, using default');
|
||||
}
|
||||
|
||||
this.mediaRecorder = new MediaRecorder(stream, options);
|
||||
|
||||
this.mediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
this.audioChunks.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
this.mediaRecorder.onstop = () => {
|
||||
this.recordingState = false;
|
||||
};
|
||||
|
||||
this.mediaRecorder.onerror = (event) => {
|
||||
console.error('MediaRecorder error:', event);
|
||||
this.recordingState = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function convertToWav(audioBlob: Blob): Promise<Blob> {
|
||||
try {
|
||||
if (audioBlob.type.includes('wav')) {
|
||||
return audioBlob;
|
||||
}
|
||||
|
||||
const arrayBuffer = await audioBlob.arrayBuffer();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
|
||||
try {
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
return audioBufferToWav(audioBuffer);
|
||||
} finally {
|
||||
audioContext.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to convert audio to WAV:', error);
|
||||
return audioBlob;
|
||||
}
|
||||
}
|
||||
|
||||
function audioBufferToWav(buffer: AudioBuffer): Blob {
|
||||
const length = buffer.length;
|
||||
const numberOfChannels = buffer.numberOfChannels;
|
||||
const sampleRate = buffer.sampleRate;
|
||||
const bytesPerSample = 2; // 16-bit
|
||||
const blockAlign = numberOfChannels * bytesPerSample;
|
||||
const byteRate = sampleRate * blockAlign;
|
||||
const dataSize = length * blockAlign;
|
||||
const bufferSize = 44 + dataSize;
|
||||
|
||||
const arrayBuffer = new ArrayBuffer(bufferSize);
|
||||
const view = new DataView(arrayBuffer);
|
||||
|
||||
const writeString = (offset: number, string: string) => {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
};
|
||||
|
||||
writeString(0, 'RIFF'); // ChunkID
|
||||
view.setUint32(4, bufferSize - 8, true); // ChunkSize
|
||||
writeString(8, 'WAVE'); // Format
|
||||
writeString(12, 'fmt '); // Subchunk1ID
|
||||
view.setUint32(16, 16, true); // Subchunk1Size
|
||||
view.setUint16(20, 1, true); // AudioFormat (PCM)
|
||||
view.setUint16(22, numberOfChannels, true); // NumChannels
|
||||
view.setUint32(24, sampleRate, true); // SampleRate
|
||||
view.setUint32(28, byteRate, true); // ByteRate
|
||||
view.setUint16(32, blockAlign, true); // BlockAlign
|
||||
view.setUint16(34, 16, true); // BitsPerSample
|
||||
writeString(36, 'data'); // Subchunk2ID
|
||||
view.setUint32(40, dataSize, true); // Subchunk2Size
|
||||
|
||||
// Cache channel arrays, write PCM via Int16Array (native little-endian, matches WAV)
|
||||
const channels: Float32Array[] = new Array(numberOfChannels);
|
||||
for (let c = 0; c < numberOfChannels; c++) {
|
||||
channels[c] = buffer.getChannelData(c);
|
||||
}
|
||||
|
||||
const pcm = new Int16Array(arrayBuffer, 44, length * numberOfChannels);
|
||||
let p = 0;
|
||||
for (let i = 0; i < length; i++) {
|
||||
for (let c = 0; c < numberOfChannels; c++) {
|
||||
let s = channels[c][i];
|
||||
if (s > 1) s = 1;
|
||||
else if (s < -1) s = -1;
|
||||
pcm[p++] = s * 0x7fff;
|
||||
}
|
||||
}
|
||||
|
||||
return new Blob([arrayBuffer], { type: MimeTypeAudio.WAV });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a File object from audio blob with timestamp-based naming
|
||||
* @param audioBlob - The audio blob to wrap
|
||||
* @param filename - Optional custom filename
|
||||
* @returns File object with appropriate name and metadata
|
||||
*/
|
||||
export function createAudioFile(audioBlob: Blob, filename?: string): File {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const extension = audioBlob.type.includes('wav') ? 'wav' : 'mp3';
|
||||
const defaultFilename = `recording-${timestamp}.${extension}`;
|
||||
|
||||
return new File([audioBlob], filename || defaultFilename, {
|
||||
type: audioBlob.type,
|
||||
lastModified: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if audio recording is supported in the current browser
|
||||
* @returns True if MediaRecorder and getUserMedia are available
|
||||
*/
|
||||
export function isAudioRecordingSupported(): boolean {
|
||||
return !!(
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.mediaDevices &&
|
||||
typeof navigator.mediaDevices.getUserMedia === 'function' &&
|
||||
typeof window !== 'undefined' &&
|
||||
window.MediaRecorder
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user