Vue.js Composables for Speech-to-Speech Translation: Building Reactive Real-Time Audio Applications

від

у

Vue.js Компоненти для перетворення мови в мову: створення реактивних реальних аудіо-додатків у режимі реального часу

https://ift.tt/SoLsvnT

Вступ: чому Vue.js Компоненти змінили все

Коли я вперше взявся за створення фронтенду сервісу реального часу перекладу мови в мову, я вже реалізував версію на React із використанням хуків. «Наскільки іншим може бути Vue?» — подумав я. Виявилося, що Composition API Vue пропонує свіжий й елегантний підхід до управління з’єднаннями WebSocket, аудіо-потоками та реактивним станом, що змусив мене переглянути структуру реальних застосунків.

У цій статті я поділюся уроками, які отримав, будуючи Vue.js композабли для реального часу аудіо сервісів перекладу, викликами, з якими зіткнувся, і паттернами, що виникли під час керування реактивними з’єднаннями WebSocket та відтворенням аудіо. Чи ви приходите з React, Angular, або є досвідченим користувачем Vue — тут знайдеться щось корисне для всіх, хто будує реальновчасові аудіо-додатки.

Виклик: потокове аудіо в реальному часі у браузері

Перш ніж переходити до коду, розглянемо, що саме ми будуємо:

– Управління з’єднанням WebSocket: встановлення та підтримка постійного з’єднання з бекендом FastAPI
– Захоплення аудіо: доступ до мікрофона користувача та передача аудіо-блоків
– Реактивний стан: синхронізація UI з станом з’єднання, результатами транскрипції та помилками
– Управління життєвим циклом: коректне очищення ресурсів при відмонтуванні компонентів
– Обробка помилок: коректне оброблення відмови у мережі, відхилення дозволів та помилок API

Краса Composition API Vue полягає в тому, що ми можемо інкапсулювати кожну з цих проблем у повторно використовуючіся, тестовані композабли.

Урок 1: Композабли — це більше, ніж React хуки з іншим синтаксисом

Якщо ви перейшли з React, спочатку думав, що композабли — це просто хуки з іншою назвою. Я помилявся. Ось у чому їхня відмінність:

Vue Композабли мають прямі реактивні посилання

// React Hook — потрібен useState
const [isConnected, setIsConnected] = useState(false);

// Vue Композаб — пряме реактивне посилання
const isConnected = ref(false);

Це може здаватися дрібницею, але має глибокі наслідки:

– Немає залежних масивів: реактивність Vue відстежує залежності автоматично
– Немає застарілих закриттів: ви завжди працюєте з поточним значенням
– Простіша розумова модель: просто мутуйте властивість .value

Життєвий цикл більш явний

// React потребує useEffect із чисткою
useEffect(() => {
// Налаштування
return () => {
// Очистка
};
}, []);

// Vue має спеціалізовані хуки життєвого циклу
onUnmounted(() => {
// Очистка
});

Урок 2: Створення WebSocket Композаблі

Дозвольте показати ядро композаблі useTranscription, яке я побудував. Це обробляє все зв’язування з нашим FastAPI бекендом через WebSocket.

Базова структура

// composables/useTranscription.ts
import { ref, onUnmounted } from ‘vue’;
import { v4 as uuidv4 } from ‘uuid’;

export function useTranscription(apiUrl = ‘ws://localhost:8000’) {
// Реактивний стан
const ws = ref(null);
const sessionId = ref(”);
const isConnected = ref(false);
const isTranscribing = ref(false);
const partialTranscript = ref(”);
const finalTranscript = ref(”);
const error = ref(null);

// Методи будуть тут…

// Очистка при розмонтуванні
onUnmounted(() => {
disconnect();
});

return {
// Стан
isConnected,
isTranscribing,
partialTranscript,
finalTranscript,
error,
sessionId,

// Методи
connect,
startTranscription,
sendAudioChunk,
stopTranscription,
disconnect,
};
}

Ключове рішення дизайну: обіцянкове з’єднання

Одна з помилок, яку я спочатку зробив — зробити з’єднання синхронним. Коли UI намагався розпочати запис одразу після з’єднання, WebSocket ще не був готовий. Ось як я це вирішив:

const connect = async (): Promise => {
return new Promise((resolve, reject) => {
try {
sessionId.value = uuidv4();
const url = `${apiUrl}/transcribe/${sessionId.value}`;

ws.value = new WebSocket(url);

ws.value.onopen = () => {
console.log(‘WebSocket connected’);
};

ws.value.onmessage = (event) => {
const message = JSON.parse(event.data);

if (message.event === ‘connected’) {
isConnected.value = true;
resolve(); // ✅ Розв’язати, коли справді готово
}
// Обробка інших повідомлень…
};

ws.value.onerror = (err) => {
error.value = ‘WebSocket connection error’;
reject(err);
};

// Таймаут через 10 секунд
setTimeout(() => {
if (!isConnected.value) {
reject(new Error(‘Connection timeout’));
}
}, 10000);
} catch (err) {
reject(err);
}
});
};

Чому це важливо: UI тепер може надійно чекати з’єднання

// У вашому компоненті
const handleConnect = async () => {
try {
await connect(); // ✅ Чекає фактичного з’єднання
await startTranscription(); // ✅ Безпечно розпочати зараз
} catch (err) {
console.error(‘Connection failed:’, err);
}
};

Урок 3: Реактивна обробка повідомлень

Краса реактивності Vue сяє під час обробки повідомлень WebSocket. Так я структурую обробку повідомлень:

const handleTranscript = (message: TranscriptResult) => {
if (message.type === ‘partial’) {
partialTranscript.value = message.transcript || ”;
} else if (message.type === ‘final’) {
finalTranscript.value += (message.transcript || ”) + ‘ ‘;
partialTranscript.value = ”; // Очистити частковий
}
};

У шаблоні це просто працює:

Ніяких ручних оновлень, немає forceUpdate, немає колбеків setState. Реактивність Vue обробляє все.

Урок 4: Композабля захоплення аудіо

Друга важлива композабля — обробка мікрофона. Тут браузерні API зустрічаються з реактивністю Vue:

// composables/useAudioCapture.ts
import { ref, onUnmounted } from ‘vue’;

export function useAudioCapture() {
const isRecording = ref(false);
const mediaRecorder = ref(null);
const audioStream = ref(null);

const startRecording = async (
onAudioData: (data: Blob) => void
): Promise => {
try {
// Запит мікрофона
audioStream.value = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1, // моно
sampleRate: 16000, // 16 кГц для мови
echoCancellation: true,
noiseSuppression: true,
},
});

mediaRecorder.value = new MediaRecorder(audioStream.value, {
mimeType: ‘audio/webm’,
audioBitsPerSecond: 16000,
});

// Потоки аудіо-часток
mediaRecorder.value.ondataavailable = (event) => {
if (event.data.size > 0) {
onAudioData(event.data); // Виклик для відправки через WebSocket
}
};

// Захоплення відбувається кожні 100мс
mediaRecorder.value.start(100);
isRecording.value = true;
} catch (err) {
console.error(‘Microphone access denied:’, err);
throw err;
}
};

const stopRecording = () => {
if (mediaRecorder.value && isRecording.value) {
mediaRecorder.value.stop();
isRecording.value = false;
}

// Важливо: зупинити всі треки, щоб звільнити мікрофон
if (audioStream.value) {
audioStream.value.getTracks().forEach((track) => track.stop());
audioStream.value = null;
}
};

// Автоматичне очищення
onUnmounted(() => {
stopRecording();
});

return {
isRecording,
startRecording,
stopRecording,
};
}

Критичний урок: очищення ресурсів

Я навчився цього болем: невтамоване зупинення медіатреків залишає мікрофон активним після розмонтування компонента. Користувачі бачать страшний індикатор «мікрофон активний» у браузері, і вони лякаються.

Завжди зупиняйте медіа-треки:

audioStream.value.getTracks().forEach((track) => track.stop());

Урок 5: Поєднання у компоненті

Ось як красиво ці композабли працюють разом:

Урок 6: Composition API проти Options API

Може виникнути питання: «Чому використовувати Composition API замість Options API?»

Ось що я виявив:

Переваги Composition API:

– Краща організація коду: пов’язані логіки залишаються разом
– Легше тестувати: композабли — це просто функції
– Підтримка TypeScript: висновок працює чудово
– Перевикористання: легко ділитися логікою між компонентами

Options API був кращим для:

– Малих, простих компонентів: іноді data() та methods ясніші
– Вивчення Vue: структура більш явна
– Командна знайомість: якщо ваша команда добре знає Options API

Для реальних застосунків із складним управлінням станом Composition API — явний переможець.

Урок 7: Обробка помилок та крайових випадків

Реальні застосунки потребують надійної обробки помилок. Ось крайові варіанти, з якими я стикався:

1. Користувач відмовляється у доступі до мікрофона

const handleStart = async () => {
try {
await startRecording(onAudioData);
} catch (err) {
if (err.name === ‘NotAllowedError’) {
error.value = ‘Дозвіл на використання мікрофона відмовлено. Дозвольте доступ.’;
} else {
error.value = ‘Не вдалося почати запис’;
}
}
};

2. Втрачено з’єднання WebSocket

ws.value.onclose = (event) => {
isConnected.value = false;
isTranscribing.value = false;

if (!event.wasClean) {
error.value = ‘З’єднання втрачено. Оновіть сторінку та спробуйте знову.’;
}
};

3. Браузерна сумісність

const checkBrowserSupport = () => {
if (!(‘MediaRecorder’ in window)) {
throw new Error(‘MediaRecorder API не підтримується’);
}

if (!(‘WebSocket’ in window)) {
throw new Error(‘WebSocket API не підтримується’);
}
};

Урок 8: Оптимізація продуктивності

Реал-тайм застосунки вимагають відмінної продуктивності. Ось що працювало:

1. Згладжування оновлень UI для часткових транскриптів

import { ref, watch } from ‘vue’;
import { debounce } from ‘lodash-es’;

const debouncedPartial = ref(”);

watch(partialTranscript, debounce((newValue) => {
debouncedPartial.value = newValue;
}, 50));

2. Використання shallowRef для великих об’єктів

import { shallowRef } from ‘vue’;

// Не робити кожну властивість реактивною
const allTranscripts = shallowRef([]);

3. Ліниве завантаження аудіо-компонентів

Урок 9: Тестування композаблі

Composition API спрощує тестування. Ось як я тестую WebSocket композаблі:

import { describe, it, expect, vi } from ‘vitest’;
import { useTranscription } from ‘../useTranscription’;

describe(‘useTranscription’, () => {
it(‘має підключитися до WebSocket’, async () => {
const { connect, isConnected } = useTranscription();

await connect();

expect(isConnected.value).toBe(true);
});

it(‘має обробляти повідомлення транскрипції’, async () => {
const { connect, partialTranscript } = useTranscription();

await connect();

// Імітувати повідомлення WebSocket
const message = {
event: ‘transcript’,
type: ‘partial’,
transcript: ‘Hello world’,
};

// Випустити повідомлення…

expect(partialTranscript.value).toBe(‘Hello world’);
});
});

Урок 10: Що б я зробив інакше

Підіб’ємо підсумок: що б я змінив:

1. Додати логіку повторного підключення раніше

Замість того щоб провалюватися через помилки з’єднання, реалізуйте експоненційне відступлення:

const connectWithRetry = async (maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) { try { await connect(); return; } catch (err) { if (i === maxRetries - 1) throw err; await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
}
}
};

2. Одразу використовувати TypeScript

Типобезпека виявила багато помилок під час рефакторингу.

3. Реалізувати Audio Queueing

Для перекладу в режимі реального часу потрібно було б чергував аудіо-вивід, щоб запобігати перекриванню відтворення.

4. Додати Логіку стану з’єднання

Керування станами з’єднання (підключення, підключено, відключення, відсутність з’єднання, помилка) полегшило б логіку.

Порівняння: Vue композабли проти React хуків

Після реалізації обох підходів, чесне порівняння:

Аспект

Vue Композабли

React хуки

Навчальний поріг

М’якший для початківців

Зміцнений (масив залежностей, замикання)

Реактивність

Автоматична, чарівна ✨

Ручна через setState

Керування посиланнями

Явне доступ до .value

Інтеграція життєвого циклу

Спеціалізовані хуки

useEffect з залежностями

Повторне використання коду

natural і інтуїтивне

Потребує обережного дизайну хуків

Підтримка TypeScript

Висновок

Для реального часу застосунків Vue з композаблами забезпечують значні переваги: автоматична реактивність зменшує ментальне навантаження, а React хуки надають більше контролю, але потребують більше boilerplate. Реальні програми з використанням моделі Composition API та TypeScript надають серйозні переваги.

Реальні результати продуктивності

Ось що вдалося досягти з цією імплементацією Vue:

– Встановлення з’єднання: у середньому ~200 мс
– Передача аудіо-блоків: інтервали 100 мс, затримка <10 мс - Оновлення UI: 60fps навіть при швидких оновленнях транскрипції - Використана пам’ять: приблизно 15 МБ для всього застосунку - Розмір бандла: близько 45КБ (gzipped, без урахування Vue) Висновок: радість від Vue композаблі Створення реальних застосунків для обробки мови в режимі реального часу за допомогою Vue-композаблі стало для мене відкриттям. Елегантна реактивність Composition API у поєднанні з типобезпекою TypeScript створюють досвід розробки, який важко перевершити. Ключові висновки: - Композаблі забезпечують чітке розділення понять — кожний композаблі відповідає за одну задачу - Реактивність просто працює — ручні оновлення стану не потрібні - Управління життєвим циклом є явним — onUnmounted робить очищення очевидним - Обіцянкові патерни інтегруються з async/await - Тестування просте — композаблі це просто функції Якщо ви будуєте реальні часопрограми у Vue, використовуйте Composition API. Ваше майбутнє «я» (і ваша команда) подякують вам. Ресурси Документація Vue Composition API WebSocket API — MDN MediaRecorder API — MDN Повні приклади коду HI-FI News via DEV Community: typescript https://ift.tt/LAExW9i 16 лютого 2026 о 18:30 February 16, 2026 at 06:30PM


Коментарі

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *