Веселимся со Spring: pet-проект по распознаванию речи

Страницы:  1

Ответить
 

Professor Seleznov


Привет Хабр !
Не писал на Spring уже лет 8 и решил по фану написать мини пет проект с api и распознаванием речи. Звучит круто, лет 8-10 назад это заняло бы … вечность, тогда и llm, достаточно качественно распознающих русскую речь, да еще на скромном домашнем пк не было. В общем решил в выходной повеселиться.
pic-Итого я собрал маленькое Spring Boot приложение, которое принимает короткий WAV-файл, отправляет его в локальную модель распознавания речи и показывает текст на странице.
Проект лежит на GitHub: https://github.com/rurikovich/RememberSpring
-
1. Что делает приложение
Сценарий простой:
  • Открываем страницу http://localhost:8080/.
  • Выбираем WAV-файл с русской речью.
  • Нажимаем кнопку перевести в текст.
  • Сервер принимает файл.
  • Vosk распознаёт речь.
  • Страница показывает результат.
pic
Ограничение специально небольшое: аудио до 10 секунд. Для учебного проекта этого хватает.
-
2. На чём написано
Стек получился такой:
  • Java 21
  • Spring Boot 4
  • Spring Web
  • Vosk Java API
  • простая HTML-страница без React и прочего фронтенд-зоопарка
  • модель vosk-model-small-ru-0.22
Vosk подключается как Maven-зависимость:

pom.xml: зависимости

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alphacephei</groupId>
<artifactId>vosk</artifactId>
<version>0.3.45</version>
</dependency>
Тут есть маленькая деталь: именно com.alphacephei:vosk, а не org.vosk:vosk.
-
3. Немного про модель
Для распознавания используется vosk-model-small-ru-0.22.
Это небольшая русская модель для Vosk. Она работает локально и не требует облачного API. То есть не нужны токены, аккаунты, лимиты запросов и оплата за каждую попытку.
Плюсы:
  • работает офлайн;
  • есть Java API;
  • легко положить рядом с учебным проектом;
  • для коротких фраз качество нормальное.
Минусы тоже есть:
  • это не большая современная модель уровня Whisper;
  • качество зависит от шума, микрофона и произношения;
  • модель занимает место в репозитории, если её коммитить.
В проекте модель лежит так:

Структура папки models

models/
vosk-model-small-ru-0.22/
am/
conf/
graph/
ivector/
README

-
4. Настройки приложения
В application.yaml задаём имя приложения, лимит загрузки файла и путь до модели.

application.yaml

spring:
application:
name: RememberSpring
servlet:
multipart:
max-file-size: 25MB
max-request-size: 25MB
asr:
max-duration-seconds: 10
vosk:
model-path: ${VOSK_MODEL_PATH:models/vosk-model-small-ru-0.22}

-
5. REST-контроллер
Контроллер здесь простой. Он принимает файл, вызывает сервис распознавания и возвращает JSON.

TranscriptionController.java

@RestController
@RequestMapping("/api")
public class TranscriptionController {
private final VoskTranscriptionService transcriptionService;
public TranscriptionController(VoskTranscriptionService transcriptionService) {
this.transcriptionService = transcriptionService;
}
@PostMapping(
value = "/transcribe",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<TranscriptionResponse> transcribe(@RequestPart("file") MultipartFile file) {
try {
TranscriptionResult result = transcriptionService.transcribe(file);
return ResponseEntity.ok(new TranscriptionResponse(
result.text(),
result.durationSeconds(),
"Vosk",
"ru"
));
} catch (IllegalArgumentException e) {
throw new ResponseStatusException(BAD_REQUEST, e.getMessage(), e);
} catch (IllegalStateException e) {
throw new ResponseStatusException(SERVICE_UNAVAILABLE, e.getMessage(), e);
}
}
}
Отдельно обрабатываются две ситуации:
  • пользователь прислал неправильный файл;
  • модель не найдена или не загрузилась.
Для учебного проекта этого достаточно. В боевом сервисе я бы добавил нормальный формат ошибок, request id и метрики.
-
6. Сервис распознавания
Основная работа находится в VoskTranscriptionService.
Что он делает:
  • проверяет, что файл не пустой;
  • лениво загружает модель Vosk;
  • читает WAV через Java Sound API;
  • приводит аудио к PCM 16 kHz mono;
  • проверяет длительность;
  • прогоняет байты через Recognizer;
  • достаёт поле text из ответа модели.

VoskTranscriptionService.java&#58; главная часть

public synchronized TranscriptionResult transcribe(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("Файл пустой. Передайте короткий .wav файл.");
}
Model loadedModel = getOrLoadModel();
try (
AudioInputStream sourceStream = AudioSystem.getAudioInputStream(
new BufferedInputStream(file.getInputStream()));
AudioInputStream pcmStream = AudioSystem.getAudioInputStream(TARGET_AUDIO_FORMAT, sourceStream);
Recognizer recognizer = new Recognizer(loadedModel, TARGET_SAMPLE_RATE)
) {
double durationSeconds = calculateDurationSeconds(sourceStream);
if (durationSeconds > maxDurationSeconds) {
throw new IllegalArgumentException(
"Файл длиннее " + maxDurationSeconds + " секунд. Укоротите запись и попробуйте снова.");
}
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = pcmStream.read(buffer)) != -1) {
if (bytesRead > 0) {
recognizer.acceptWaveForm(buffer, bytesRead);
}
}
String finalResultJson = recognizer.getFinalResult();
return new TranscriptionResult(extractText(finalResultJson), durationSeconds);
} catch (UnsupportedAudioFileException e) {
throw new IllegalArgumentException("Неподдерживаемый формат аудио. Для демо используйте WAV.", e);
} catch (IOException e) {
throw new IllegalStateException("Не удалось прочитать аудиофайл.", e);
}
}
Почему метод synchronized? Чтобы в маленьком учебном приложении не ловить одновременную работу нескольких Recognizer на одном сервисе и не усложнять код пулом задач. Для одного пользователя и демо-страницы нормально.
В продакшене я бы так не оставил. Там уже нужны очередь, worker-пул и ограничения по нагрузке.
-
7. HTML-страница
Фронтенд тут специально минимальный. Один index.html в src/main/resources/static.
Spring Boot сам отдаёт его по адресу http://localhost:8080/.

index.html&#58; отправка файла

<form id="transcriptionForm">
<label for="audioFile">Аудиофайл</label>
<input id="audioFile" name="file" type="file" accept="audio/wav,.wav" required>
<button id="submitButton" type="submit">перевести в текст</button>
</form>
<div id="result" class="result" hidden></div>
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const response = await fetch('/api/transcribe', {
method: 'POST',
body: formData
});
const payload = await response.json();
showResult(payload.text || 'Модель не распознала текст.', false);
Без сборки фронтенда. Без npm. Без отдельного dev-server.
Для учебной статьи это плюс. Меньше магии вокруг, проще увидеть саму механику: файл ушёл на сервер, сервер вернул текст.
-
8. Как запустить
Клонируем проект:

Команды запуска

git clone https://github.com/rurikovich/RememberSpring.git
cd RememberSpring
./mvnw spring-boot:run
Потом открываем:
http://localhost:8080/
Выбираем WAV-файл до 10 секунд и нажимаем кнопку.
Если хочется проверить без страницы:

curl-запрос

curl -X POST "http://localhost:8080/api/transcribe" \
-F "file=@sample.wav"
Ответ будет примерно такой:

Пример JSON-ответа

{
"text": "тестовое сообщение для тестового проекта длиной пять секунд",
"durationSeconds": 5.0,
"engine": "Vosk",
"language": "ru"
}

-
9. Что можно улучшить
Для учебного проекта всё ок. Но если делать нормальный сервис, я бы первым делом поменял несколько вещей.
Во-первых, не выполнять распознавание прямо в HTTP-запросе. Лучше принимать файл, отдавать jobId, а распознавание делать в фоне.
Во-вторых, добавить поддержку форматов через отдельный декодер. Сейчас демо работает только с WAV. А пользователь легко принесёт OGG, MP3 или что-то из мессенджера.
В-третьих, модель не всегда стоит коммитить в репозиторий. Для статьи это удобно: скачал проект, запустил, всё рядом. Для рабочего проекта модель лучше хранить как отдельный артефакт.
Ну и метрики. Без них не понятно: модель долго думает, файл плохой, запись шумная или сервер просто не тянет.
-
10. Итог
Получился маленький, но интересный пример:
  • Spring принимает файл;
  • Vosk распознаёт русскую речь локально;
  • HTML-страница показывает результат;
  • код можно запустить без облачных ключей.
Мне такие учебные проекты нравятся больше абстрактных примеров. Они не очень большие, но в них уже есть настоящие проблемы: лимит загрузки файла, путь до модели, формат аудио, обработка ошибок.-Если вам близки темы разработки, рефакторинга, архитектуры и стартапов буду рад видеть вас в моём Telegram-канале.
Веселых Вам Выходных !-Источник
 
Loading...
Error