惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MongoDB | Blog
MongoDB | Blog
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
D
DataBreaches.Net
腾讯CDC
GbyAI
GbyAI
I
InfoQ
博客园 - Franky
G
Google Developers Blog
Last Week in AI
Last Week in AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Visual Studio Blog
Vercel News
Vercel News
博客园_首页
MyScale Blog
MyScale Blog
Martin Fowler
Martin Fowler
N
Netflix TechBlog - Medium
V
V2EX
T
The Blog of Author Tim Ferriss
M
MIT News - Artificial intelligence
雷峰网
雷峰网
H
Hackread – Cybersecurity News, Data Breaches, AI and More
大猫的无限游戏
大猫的无限游戏
The GitHub Blog
The GitHub Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Веселимся со Spring: pet-проект по распознаванию речи
rurikovich · 2026-05-10 · via Все публикации подряд на Хабре

Привет Хабр !

Не писал на 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: 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: главная часть
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.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-канале.

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

0%да0

0%это что то на программистском0

Никто еще не голосовал. Воздержавшихся нет.