в

Приложение для преобразования речи в текст на Next.js 14 и OpenAI API

15 апреля 2024
Приложение для преобразования речи в текст на Next.js 14 и OpenAI API

Оглавление:

  1. Инициализация проекта
  • Инициализация Next.js
  • Установка необходимых зависимостей, таких как openai
  1. Создание хука useRecordVoice
  • Разработка хука для записи голоса
  • Использование API MediaRecorder
  1. Создание компонента кнопки
  • Создание компонента кнопки для управления записью голоса
  1. Проверка функциональности
  • Запуск проекта и проверка основных функций
  1. Настройка API распознавания речи
  • Создание API-эндпоинта для обработки аудиоданных и взаимодействия с API OpenAI
  1. Дополнительные функции и улучшения
  • Разработка вспомогательных функций для аудио и преобразования в base64
  • Реализация дополнительных функций для получения текстового вывода

Шаг 1. Инициализация проекта

Для начала инициализируйте проект Next.js и установите необходимые зависимости, включая openai.

Инициализация проекта Next.js

npx create-next-app my-voice-app

Установка необходимых зависимостей: openai v4, dotenv

npm install openai dotenv

Шаг 2. Создание хука useRecordVoice

Разработка хука для записи голоса с использованием API MediaRecorder.

import { useEffect, useState, useRef } from "react";

export const useRecordVoice = () => {
  // Состояние для хранения экземпляра MediaRecorder
  const [mediaRecorder, setMediaRecorder] = useState(null);

  // Состояние для отслеживания, идет ли запись в данный момент
  const [recording, setRecording] = useState(false);

  // Ссылка для хранения аудио-фрагментов во время записи
  const chunks = useRef([]);

  // Функция для начала записи
  const startRecording = () => {
    if (mediaRecorder) {
      mediaRecorder.start();
      setRecording(true);
    }
  };

  // Функция для остановки записи
  const stopRecording = () => {
    if (mediaRecorder) {
      mediaRecorder.stop();
      setRecording(false);
    }
  };

  // Функция для инициализации MediaRecorder
  const initialMediaRecorder = (stream) => {
    const mediaRecorder = new MediaRecorder(stream);

    // Обработчик события при начале записи
    mediaRecorder.onstart = () => {
      chunks.current = []; // Сброс массива фрагментов
    };

    // Обработчик события при доступности данных во время записи
    mediaRecorder.ondataavailable = (ev) => {
      chunks.current.push(ev.data); // Сохранение фрагментов данных
    };

    // Обработчик события при остановке записи
    mediaRecorder.onstop = () => {
      // Создание Blob из накопленных аудио-фрагментов в формате WAV
      const audioBlob = new Blob(chunks.current, { type: "audio/wav" });
      console.log(audioBlob, 'audioBlob');

      // Вы можете выполнить какие-либо действия с audioBlob, например, отправить его на сервер или дальнейшую обработку
    };

    setMediaRecorder(mediaRecorder);
  };

  useEffect(() => {
    if (typeof window !== "undefined") {
      navigator.mediaDevices
        .getUserMedia({ audio: true })
        .then(initialMediaRecorder);
    }
  }, []);

  return { recording, startRecording, stopRecording };
};

Шаг 3. Создание компонента Button

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

import { useRecordVoice } from "@/hooks/useRecordVoice";
import { IconMicrophone } from "@/app/components/IconMicrophone";

const Microphone = () => {
  const { startRecording, stopRecording } = useRecordVoice();

  return (
    // Кнопка для начала и остановки записи голоса
    <button
      onMouseDown={startRecording}    // Начать запись при нажатии кнопки мыши
      onMouseUp={stopRecording}       // Остановить запись при отпускании кнопки мыши
      onTouchStart={startRecording}   // Начать запись при начале касания на сенсорном устройстве
      onTouchEnd={stopRecording}      // Остановить запись при окончании касания на сенсорном устройстве
      className="border-none bg-transparent w-10"
    >
      {/* Компонент иконки микрофона */}
      <IconMicrophone />
    </button>
  );
};

export { Microphone };

Шаг 4. Проверка функциональности

Запустите проект и проверьте основную функциональность.

npm run dev

Откройте http://localhost:3000/ в вашем браузере и убедитесь, что появляется запрос на разрешение доступа к микрофону.

После этого на вкладке браузера должен загореться индикатор микрофона, означающий, что вы все сделали правильно.

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

5. Настройка API распознавания речи

Создайте конечную точку API для обработки аудиоданных и взаимодействия с API OpenAI.

В папке src/app/api/speechToText/route.js создадим API speechToText.

import { NextResponse } from "next/server";
import fs from "fs";
import * as dotenv from "dotenv";
import OpenAI from "openai";
import { env } from "../../config/env";

dotenv.config();

const openai = new OpenAI({
  apiKey: env.OPENAI_API_KEY,
});

export async function POST(req) {
  const body = await req.json();
  const base64Audio = body.audio;

  // Преобразование base64 аудиоданных в Buffer
  const audio = Buffer.from(base64Audio, "base64");

  // Определение пути к файлу для хранения временного WAV-файла
  const filePath = "tmp/input.wav";

  try {
    // Синхронная запись аудиоданных во временный WAV-файл
    fs.writeFileSync(filePath, audio);

    // Создание потока чтения из временного WAV-файла
    const readStream = fs.createReadStream(filePath);

    const data = await openai.audio.transcriptions.create({
      file: readStream,
      model: "whisper-1"
    });

    // Удаление временного файла после успешной обработки
    fs.unlinkSync(filePath);

    return NextResponse.json(data);
  } catch (error) {
    console.error("Ошибка обработки аудио:", error);
    return NextResponse.error();
  }
}

Вкратце, здесь мы создаем файл, в который записываем base64Audio, затем читаем его и после успешного запроса удаляем файл.

Также убедитесь, что вы создали папку tmp на том же уровне, что и src, и добавили .gitignore, чтобы вы могли фиксировать и отправлять эту папку.

6. Дополнительные функции и улучшения

Добавим вспомогательные функции для преобразования аудио и base64, а также улучшим функциональность.

blobToBase64

// callback - где мы хотим получить результат
const blobToBase64 = (blob, callback) => {
  const reader = new FileReader();

  reader.onload = function () {
    const base64data = reader?.result?.split(",")[1];
    callback(base64data);
  };

  reader.readAsDataURL(blob);
};

export { blobToBase64 };

createMediaStream

// Функция для вычисления пикового уровня из данных анализатора
const getPeakLevel = (analyzer) => {
  // Создание Uint8Array для хранения аудиоданных
  const array = new Uint8Array(analyzer.fftSize);

  // Получение данных временной области из анализатора и сохранение их в массиве
  analyzer.getByteTimeDomainData(array);

  // Вычисление пикового уровня путем нахождения максимального абсолютного отклонения от 127
  return (
    array.reduce((max, current) => Math.max(max, Math.abs(current - 127)), 0) /
    128
  );
};

const createMediaStream = (stream, isRecording, callback) => {
  // Создание нового AudioContext
  const context = new AudioContext();

  // Создание узла источника медиа-потока из входного потока
  const source = context.createMediaStreamSource(stream);

  // Создание узла анализатора для анализа аудио
  const analyzer = context.createAnalyser();

  // Подключение узла источника к узлу анализатора
  source.connect(analyzer);

  // Функция для непрерывного анализа аудиоданных и вызова обратного вызова
  const tick = () => {
    // Вычисление пикового уровня с помощью функции getPeakLevel
    const peak = getPeakLevel(analyzer);
    if (isRecording) {
      callback(peak);
      // Запрос следующего кадра анимации для непрерывного анализа
      requestAnimationFrame(tick);
    }
  };

  // Запуск цикла непрерывного анализа
  tick();
};

export { createMediaStream };

Вы можете узнать больше о встроенном методе JavaScript requestAnimationFrame здесь.

Теперь используем наши вспомогательные функции и speechToText API в useRecordVoice.

"use client";
import { useEffect, useState, useRef } from "react";
import { blobToBase64 } from "@/utils/blobToBase64";
import { createMediaStream } from "@/utils/createMediaStream";

export const useRecordVoice = () => {
  const [text, setText] = useState("");
  const [mediaRecorder, setMediaRecorder] = useState(null);
  const [recording, setRecording] = useState(false);
  const isRecording = useRef(false);
  const chunks = useRef([]);

  const startRecording = () => {
    if (mediaRecorder) {
      isRecording.current = true;
      mediaRecorder.start();
      setRecording(true);
    }
  };

  const stopRecording = () => {
    if (mediaRecorder) {
      isRecording.current = false;
      mediaRecorder.stop();
      setRecording(false);
    }
  };

  const getText = async (base64data) => {
    try {
      const response = await fetch("/api/speechToText", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          audio: base64data,
        }),
      }).then((res) => res.json());
      const { text } = response;
      setText(text);
    } catch (error) {
      console.log(error);
    }
  };

  const initialMediaRecorder = (stream) => {
    const mediaRecorder = new MediaRecorder(stream);

    mediaRecorder.onstart = () => {
      createMediaStream(stream, isRecording.current, () => {});
      chunks.current = [];
    };
    mediaRecorder.ondataavailable = (ev) => {
      chunks.current.push(ev.data);
    };
    mediaRecorder.onstop = () => {
      const audioBlob = new Blob(chunks.current, { type: "audio/wav" });
      blobToBase64(audioBlob, getText);
    };
    setMediaRecorder(mediaRecorder);
  };

  useEffect(() => {
    if (typeof window !== "undefined") {
      navigator.mediaDevices
        .getUserMedia({ audio: true })
        .then(initialMediaRecorder);
    }
  }, []);

  return { recording, startRecording, stopRecording, text };
};

После этого перейдите в компонент Microphone и добавьте вывод нашего текста.

const Microphone = () => {
  const { startRecording, stopRecording, text } = useRecordVoice();

  return (
    <div className="flex flex-col justify-center items-center">
      <button
        onMouseDown={startRecording}
        onMouseUp={stopRecording}
        onTouchStart={startRecording}
        onTouchEnd={stopRecording}
        className="border-none bg-transparent w-10"
      >
        <IconMicrophone />
      </button>
      <p>{text}</p>
    </div>
  );
};

export { Microphone };

Сохраните изменения и перезапустите сервер.

Затем перейдите на http://localhost:3000/ и выполните следующие действия:

  1. Нажмите кнопку микрофона.
  2. Скажите: "Hello world".

Все готово! Теперь в вашем приложении Next.js есть полностью функциональный голосовой ввод.

Теги: