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

Итого я собрал маленькое Spring Boot приложение, которое принимает короткий WAV-файл, отправляет его в локальную модель распознавания речи и показывает текст на странице.
Проект лежит на GitHub: https://github.com/rurikovich/RememberSpring
1. Что делает приложение
Сценарий простой:
-
Открываем страницу
http://localhost:8080/. -
Выбираем WAV-файл с русской речью.
-
Нажимаем кнопку
перевести в текст. -
Сервер принимает файл.
-
Vosk распознаёт речь.
-
Страница показывает результат.

Ограничение специально небольшое: аудио до 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/