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

от автора

Привет Хабр !

Не писал на Spring уже лет 8 и решил по фану написать мини пет проект с api и распознаванием речи. Звучит круто, лет 8-10 назад это заняло бы … вечность, тогда и llm, достаточно качественно распознающих русскую речь, да еще на скромном домашнем пк не было. В общем решил в выходной повеселиться.


Итого я собрал маленькое Spring Boot приложение, которое принимает короткий WAV-файл, отправляет его в локальную модель распознавания речи и показывает текст на странице.

Проект лежит на GitHub: https://github.com/rurikovich/RememberSpring


1. Что делает приложение

Сценарий простой:

  1. Открываем страницу http://localhost:8080/.

  2. Выбираем WAV-файл с русской речью.

  3. Нажимаем кнопку перевести в текст.

  4. Сервер принимает файл.

  5. Vosk распознаёт речь.

  6. Страница показывает результат.

Ограничение специально небольшое: аудио до 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: 25MBasr:  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: главная часть
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: отправка файла
<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.gitcd 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-канале.

Веселых Вам Выходных !

ссылка на оригинал статьи https://habr.com/ru/articles/1033338/