diff --git a/pom.xml b/pom.xml
index 8faba20..61e502b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,47 +1,78 @@
+ * Класс-репозиторий, который умеет подключаться к OpenAI API и задавать вопросы модели + *
+ */ +@Slf4j +public class AIModel { + private OpenAIModels model; + private final String apiKey; + private final static String URL = "https://api.openai.com/v1/chat/completions"; + + /** + *Создает объект этого класса, который делает запросы к модели
+ * @param apiKey ключ, с помощью которого можно сделать запрос + */ + public AIModel(String apiKey) { + this.apiKey = apiKey; + this.model = OpenAIModels.CHAT_GPT_3_5; + } + + /** + *Делает запрос к OpenAI API к той модели, которая установлена в соответствующем поле
+ * @param messages предыдущие сообщения в переписке + * @param mess текущий вопрос к модели + * @return возвращает ответ модели типаModelResponse
+ */
+ protected ModelResponse ask(List+ * Класс-сервис, который настраивает модель соответствующим образом и хранит в себе контекст пользователя + * в течении одного часа, который помогает модели ориентироваться в сообщениях пользователя + *
+ *+ * Также класс умеет само очищаться если время последнего взаимодействия пользователя + * модели с пользователем была более часа назад + *
+ */ +@Slf4j +public class AIService { + private final AIModel model; + private final Map+ * Перед запросом настраивает модели если это необходимо и подключается к OpenAI API, + * дожидается ответ и возвращает его + *
+ * @param userId id пользователя, который запросил ответ на свой вопрос + * @param message сам вопрос + * @return ответ на вопрос, который хранится в объекте типаModelResponse
+ */
+ public ModelResponse ask(int userId, String message) {
+ synchronized (usersMessages) {
+ synchronized (modelLastInteraction) {
+ if (usersMessages.get(userId) == null) {
+ usersMessages.put(userId, new ArrayList<>());
+ modelLastInteraction.put(userId, new Date());
+ }
+ }
+
+ if (usersMessages.get(userId).isEmpty()) {
+ usersMessages.get(userId).add(new OpenAIMessage(
+ "user",
+ "Вы являетесь голосовым помощником под названием Jarvis. " +
+ "Нужно стараться давать ответ как можно короче, но максимально информативным. " +
+ "Формулировать ответы нужно в манере искусственного интеллекта Jarvis из фильмов " +
+ "\"Железный человек\". Реагировать на осуждение твоих ответов нужно в стиле Jarvis " +
+ "и ссылаться на то, что ты всего лишь программа, написанная другим программистом\n" +
+ "Каждый ответ должен укладываться в 5000 символов. Отвечать на это сообщение не надо")
+ );
+ }
+ }
+
+ ModelResponse response = model.ask(usersMessages.get(userId), message);
+
+ if (response == null) {
+ return null;
+ }
+
+ synchronized (usersMessages) {
+ synchronized (modelLastInteraction) {
+ usersMessages.get(userId).add(new OpenAIMessage(OpenAIRole.USER.getRole(), message));
+
+ for (OpenAIChoices choice : response.getChoices()) {
+ if (choice.getMessage().getRole().equals(OpenAIRole.ASSISTANT.getRole()))
+ usersMessages.get(userId).add(choice.getMessage());
+ }
+
+ modelLastInteraction.replace(userId, new Date());
+ }
+ }
+
+ return response;
+ }
+
+ /**
+ * Возвращает контекст со всеми пользователями, где ключ это userId, а значение это контекст
+ * @return контекст со всеми пользователями + */ + protected Map+ * Возвращает дату последнего взаимодействия для каждого контекста, + * где ключ это userId, а значение дата последнего взаимодействия + *
+ *Если контекста для пользователя не существует, то будет возвращено null
+ * @return сло + */ + protected MapУдаляет контекст для конкретного пользователя
+ * @param userId id пользователя, контекст которого надо удалить + */ + public void clearMessages(int userId) { + synchronized (usersMessages) { + synchronized (modelLastInteraction) { + log.info("For user with id=" + userId + " history chat cleared"); + usersMessages.remove(userId); + modelLastInteraction.remove(userId); + } + } + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/ClearMessagesGarber.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/ClearMessagesGarber.java new file mode 100644 index 0000000..de39a25 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/ClearMessagesGarber.java @@ -0,0 +1,69 @@ +package com.iffomko.voiceAssistant.APIs.openAI; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.Map; + +/** + *+ * Этот класс отвечает за очистку сообщений в объектах AIService. + * Он очищает сообщения если срок последнего взаимодействия в этом чате на текущий момент больше 1 часа. + *
+ *
+ * Этот класс реализуется интерфейс Runnable, следовательно его нужно запускать в отдельном потоке,
+ * потому что иначе программа просто зависнет.
+ *
+ * Конструктор класса ClearMessagesGarber. Он устанавливает время через которое этот класс будет
+ * производить очистку, а также сам сервис, в котором будет производится очистка.
+ *
+ * Когда объект реализуется интерфейс {@code Runnable} этот метод используется + * для создания потока, при запуске потока {@code run} метод этого объекта будет вызван + * в отдельно исполняющем потоке + *
+ */ + @Override + public void run() { + while (true) { + synchronized (service.getUsersMessages()) { + synchronized (service.getModelLastInteraction()) { + for (Map.EntryОбъект, который конвертируется в JSON для передачи в теле запроса к OpenAI API
+ */ +@Data +public class ModelBodyDTO { + private String model; + private Double temperature; + private ListОбъект, который хранит в себе ответ от OpenAI API
+ */ +@Data +public class ModelResponse { + private String id; + private String object; + private Integer created; + private String model; + private OpenAIUsage usage; + private List+ * Объект, который хранит один ответ на вопрос от OpenAI API: + * сообщение, причина завершения и индекс в массиве таких ответов (ответов может быть несколько) + *
+ */ +@Data +public class OpenAIChoices { + private OpenAIMessage message; + private String finishReason; + private Integer index; + + public OpenAIChoices() {} + + public OpenAIChoices(OpenAIMessage message, String finishReason, Integer index) { + this.message = message; + this.finishReason = finishReason; + this.index = index; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIMessage.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIMessage.java new file mode 100644 index 0000000..fbac4bc --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIMessage.java @@ -0,0 +1,19 @@ +package com.iffomko.voiceAssistant.APIs.openAI.data; + +import lombok.Data; + +/** + *Объект, который хранит в себе информацию о сообщении в ответе на вопрос от OpenAI API
+ */ +@Data +public class OpenAIMessage { + private String role; + private String content; + + public OpenAIMessage() {} + + public OpenAIMessage(String role, String content) { + this.role = role; + this.content = content; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIUsage.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIUsage.java new file mode 100644 index 0000000..b4c9742 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/data/OpenAIUsage.java @@ -0,0 +1,21 @@ +package com.iffomko.voiceAssistant.APIs.openAI.data; + +import lombok.Data; + +/** + *Объект, который хранит в себе количество токенов вопроса, ответа и их сумму
+ */ +@Data +public class OpenAIUsage { + private Integer promptTokens; + private Integer completionTokens; + private Integer totalTokens; + + public OpenAIUsage() {} + + public OpenAIUsage(Integer promptTokens, Integer completionTokens, Integer totalTokens) { + this.promptTokens = promptTokens; + this.completionTokens = completionTokens; + this.totalTokens = totalTokens; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIModels.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIModels.java new file mode 100644 index 0000000..9974f37 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIModels.java @@ -0,0 +1,25 @@ +package com.iffomko.voiceAssistant.APIs.openAI.types; + +/** + *Перечисление всех моделей доступных для обращение к API OpenAI и их "температура"
+ */ +public enum OpenAIModels { + CHAT_GPT_3_5("gpt-3.5-turbo", 0.6), + CHAT_GPT_4("gpt-4", 0.6); + + private final String model; + private final Double temperature; + + OpenAIModels(String model, Double temperature) { + this.model = model; + this.temperature = temperature; + } + + public String getModel() { + return model; + } + + public Double getTemperature() { + return temperature; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIRole.java b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIRole.java new file mode 100644 index 0000000..da0c737 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/openAI/types/OpenAIRole.java @@ -0,0 +1,20 @@ +package com.iffomko.voiceAssistant.APIs.openAI.types; + +/** + *Перечисление, которое хранит в себе все роли, которые могут быть у сообщения
+ */ +public enum OpenAIRole { + USER("user"), + ASSISTANT("assistant"), + SYSTEM("system"); + + private final String role; + + OpenAIRole(String role) { + this.role = role; + } + + public String getRole() { + return role; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexClient.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexClient.java new file mode 100644 index 0000000..f247170 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexClient.java @@ -0,0 +1,60 @@ +package com.iffomko.voiceAssistant.APIs.speech; + +import com.iffomko.voiceAssistant.APIs.speech.data.RecognitionResponse; + +/** + *Клиент, который позволяет обращаться к Yandex API за распознаванием и синтезированием голоса
+ */ +public class YandexClient { + private final YandexRecognition recognition; + private final YandexSynthesis synthesis; + + /** + *Создает класс YandexClient и заполняет его поля нужными значениями
+ * @param apiKey ключ от Yandex API + */ + public YandexClient(String apiKey) { + this.recognition = new YandexRecognition(apiKey); + this.synthesis = new YandexSynthesis(apiKey); + } + + /** + *+ * Возвращает объект, который занимается распознаванием голоса. С помощью этого метода можно только настроить + * этот объект по свои нужды. + *
+ * @return объект для распознавания голоса + */ + public YandexRecognition getRecognition() { + return recognition; + } + + /** + *+ * Возвращает объект, который занимается синтезированием голоса. С помощью этого метода можно только настроить + * этот объект по свои нужды. + *
+ * @return объект для синтезирования голоса + */ + public YandexSynthesis getSynthesis() { + return synthesis; + } + + /** + *Метод, который позволяет распознать речь в входящем аудио
+ * @param voice входящее аудио в виде массива байтов + * @return объектRecognitionResponse, в котором содержится в поле result распознанный текст
+ */
+ public RecognitionResponse recognise(byte[] voice) {
+ return recognition.recognition(voice);
+ }
+
+ /**
+ * Метод, который позволяет синтезировать речь
+ * @param text текст, который надо озвучить + * @return массив байтов синтезированной речи + */ + public byte[] synthesis(String text) { + return synthesis.synthesize(text); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexRecognition.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexRecognition.java new file mode 100644 index 0000000..3aa91b2 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexRecognition.java @@ -0,0 +1,181 @@ +package com.iffomko.voiceAssistant.APIs.speech; + +import com.iffomko.voiceAssistant.APIs.speech.data.RecognitionResponse; +import com.iffomko.voiceAssistant.APIs.speech.types.YandexLanguage; +import com.iffomko.voiceAssistant.APIs.speech.types.YandexTopic; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; + +/** + *Класс, который умеет делать запросы к Yandex API и с её помощью синтезировать речь
+ */ +@Slf4j +public class YandexRecognition { + private final String apiKey; + private YandexLanguage lang = null; + private YandexTopic topic = null; + private boolean profanityFilter = false; + private String format = "oggopus"; + public final static String URL = "https://stt.api.cloud.yandex.net/speech/v1/stt:recognize"; + + /** + *Конструктор, который принимает ключ от Yandex API, чтобы можно авторизоваться с его помощью в системе
+ * @param apiKey ключ от Yandex API для авторизации + */ + public YandexRecognition(String apiKey) { + this.apiKey = apiKey; + } + + /** + *Формирует строчку для Query параметров для запроса
+ * @return строка содержащая query параметры + */ + private String getQueryParams() { + StringBuilder queries = new StringBuilder(); + + if (lang != null) { + queries.append("lang="); + queries.append(lang.getLang()); + } + + if (topic != null) { + if (!queries.isEmpty()) { + queries.append("&"); + } + + queries.append("topic="); + queries.append(topic.getTopic()); + } + + if (!queries.isEmpty()) { + queries.append("&"); + } + + queries.append("profanityFilter="); + queries.append(profanityFilter); + + if (!queries.isEmpty()) { + queries.append("&"); + } + + queries.append("format="); + queries.append(format); + + return queries.toString(); + } + + /** + *Делает запрос к Yandex API за распознаванием речи по входному аудио
+ * @param voice массив байтов аудио, которое нужно распознать + * @return объектRecognitionResponse, содержащий единственное поле result с распознанным текстом
+ */
+ protected RecognitionResponse recognition(byte[] voice) {
+ RestTemplate restTemplate = new RestTemplate();
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
+ headers.set("Authorization", "Api-Key " + apiKey);
+
+ HttpEntityВозвращает язык, который нужно распознавать, в настройках этого класса
+ * @return объектYandexLanguage, который содержит распознаваемый язык
+ */
+ public YandexLanguage getLang() {
+ return lang;
+ }
+
+ /**
+ * Устанавливает распознаваемый язык для текущих настроек класса
+ * @param lang объектYandexLanguage, содержащий распознаваемый язык
+ */
+ public void setLang(YandexLanguage lang) {
+ this.lang = lang;
+ }
+
+ /**
+ * Возвращает название модели распознавания в настройках этого класса
+ * @return объектYandexTopic, содержащий название модели распознавания
+ */
+ public YandexTopic getTopic() {
+ return topic;
+ }
+
+ /**
+ * Устанавливает модель распознавания для настроек этого класса
+ * @param topic объектYandexTopic, содержащий название модели распознавания
+ */
+ public void setTopic(YandexTopic topic) {
+ this.topic = topic;
+ }
+
+ /**
+ *
+ * Возвращает значение поля profanityFilter.
+ * Если установлен false, то ненормативная лексика не будет исключена из распознавания.
+ * Если установлен true, то нормативная лекция будет исключена из распознавания.
+ *
+ * Устанавливает значение true или false для поля profanityFilter.
+ * Если установлен false, то ненормативная лексика не будет исключена из распознавания.
+ * Если установлен true, то нормативная лекция будет исключена из распознавания.
+ *
true или false - значение, которое нужно установить полю
+ */
+ public void setProfanityFilter(@NonNull boolean profanityFilter) {
+ this.profanityFilter = profanityFilter;
+ }
+
+ /**
+ * Возвращает формат входного аудио (mp3, oggopus, lpcm)
+ * @return возвращает формат входящего аудио + */ + public String getFormat() { + return format; + } + + /** + *Устанавливает формат входного аудио в настройках этого класса
+ * @param format формат входящего аудио (mp3, oggopus, lpcm) + */ + public void setFormat(@NonNull String format) { + this.format = format; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexSynthesis.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexSynthesis.java new file mode 100644 index 0000000..5907914 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/YandexSynthesis.java @@ -0,0 +1,99 @@ +package com.iffomko.voiceAssistant.APIs.speech; + +import com.iffomko.voiceAssistant.APIs.speech.types.YandexVoice; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +/** + *Класс, который умеет делать запросы к Yandex API за синтезом речи
+ */ +@Slf4j +public class YandexSynthesis { + private final String apiKey; + private YandexVoice voice = YandexVoice.FILIPP; + private String format = "oggopus"; + + public static final String URL = "https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize\n"; + + /** + *Конструктор, который принимает ключ от Yandex API, чтобы можно авторизоваться с его помощью в системе
+ * @param apiKey ключ от Yandex API для авторизации + */ + public YandexSynthesis(String apiKey) { + this.apiKey = apiKey; + } + + /** + *Делает запрос на Yandex API, чтобы синтезировать речь по тексту
+ * @param text текст, который необходимо озвучить + * @return массив байтов озвученного текста + */ + protected byte[] synthesize(@NonNull String text) { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + headers.set("Authorization", "Api-Key " + apiKey); + + MultiValueMapВозвращает голос для синтеза речи, установленный в настройках этого класса
+ * @return объектYandexVoice, в котором хранится текущий голос в настройках
+ */
+ public YandexVoice getVoice() {
+ return voice;
+ }
+
+ /**
+ * Устанавливает голос для синтеза речи
+ * @param voice голос для синтеза речи, который нужно установить + */ + public void setVoice(@NonNull YandexVoice voice) { + this.voice = voice; + } + + /** + *Возвращает формат выходной аудиодорожки (mp3, oggopus, lpcm)
+ * @return формат аудиодорожки + */ + public String getFormat() { + return format; + } + + /** + *Устанавливает формат для выходной аудиодорожки (mp3, oggopus, lpcm)
+ * @param format формат аудиодорожки + */ + public void setFormat(@NonNull String format) { + this.format = format; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/speech/data/RecognitionResponse.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/data/RecognitionResponse.java new file mode 100644 index 0000000..99887fa --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/data/RecognitionResponse.java @@ -0,0 +1,11 @@ +package com.iffomko.voiceAssistant.APIs.speech.data; + +import lombok.Data; + +/** + *Объект ответа, в котором содержится одно единственное поле result, которое содержит распознанный текст
+ */ +@Data +public class RecognitionResponse { + private String result; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexFormat.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexFormat.java new file mode 100644 index 0000000..92380ac --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexFormat.java @@ -0,0 +1,24 @@ +package com.iffomko.voiceAssistant.APIs.speech.types; + +/** + *Перечисление, которое содержит доступные форматы для аудио
+ */ +public enum YandexFormat { + MP3("mp3"), + OGGOPUS("oggopus"), + LPCM("lpcm"); + + private final String format; + + YandexFormat(String format) { + this.format = format; + } + + /** + *Возвращает формат для аудио
+ * @return формат + */ + public String getFormat() { + return format; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexLanguage.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexLanguage.java new file mode 100644 index 0000000..8d32653 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexLanguage.java @@ -0,0 +1,61 @@ +package com.iffomko.voiceAssistant.APIs.speech.types; + +/** + *Перечисление всех возможных языков доступных для распознавания для запроса
+ */ +public enum YandexLanguage { + /** + *Русский язык
+ */ + RUSSIAN("ru-RU"), + /** + *Английский язык
+ */ + ENGLISH("en-US"), + /** + *Немецкий язык
+ */ + GERMANY("de-DE"), + /** + *Испанский язык
+ */ + SPANISH("es-ES"), + /** + *Французский язык
+ */ + FRENCH("fr-FR"), + /** + *Казахский язык
+ */ + KAZAKH("kk-KK"), + /** + *Тюркский язык
+ */ + TURKISH("tr-TR"), + /** + *Польский язык
+ */ + POLISH("pl-PL"), + /** + *Итальянский язык
+ */ + ITALIAN("it-IT"); + + private final String lang; + + /** + *Устанавливает язык для распозновательной модели
+ * @param lang - язык + */ + YandexLanguage(String lang) { + this.lang = lang; + } + + /** + *Возвращает язык для распозновательной модели
+ * @return - язык + */ + public String getLang() { + return lang; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexTopic.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexTopic.java new file mode 100644 index 0000000..0ee27ee --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexTopic.java @@ -0,0 +1,37 @@ +package com.iffomko.voiceAssistant.APIs.speech.types; + +/** + *Перечисление всех версий моделей распознавания доступных для запроса
+ */ +public enum YandexTopic { + /** + *Основная версия модели
+ */ + GENERAL("general"), + /** + *Версия-кандидат для релиза, которую вы можете тестировать
+ */ + GENERAL_RC("general:rc"), + /** + *Предыдущая версия модели
+ */ + GENERAL_DEPRECATED("general:deprecated"); + + private final String topic; + + /** + *Устанавливает текущее значение распозновательной модели
+ * @param topic - версия модели + */ + YandexTopic(String topic) { + this.topic = topic; + } + + /** + *Возвращает тег распазновательной модели
+ * @return - тег модели + */ + public String getTopic() { + return topic; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexVoice.java b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexVoice.java new file mode 100644 index 0000000..ef2de61 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/APIs/speech/types/YandexVoice.java @@ -0,0 +1,42 @@ +package com.iffomko.voiceAssistant.APIs.speech.types; + +/** + *Перечисление всех голосов, которые доступны для синтеза голоса
+ */ +public enum YandexVoice { + FILIPP("filipp", YandexLanguage.RUSSIAN), + ALENA("alena", YandexLanguage.RUSSIAN), + ERMIL("ermil", YandexLanguage.RUSSIAN), + JANE("jane", YandexLanguage.RUSSIAN), + MADIRUS("madirus", YandexLanguage.RUSSIAN), + OMAZH("omazh", YandexLanguage.RUSSIAN), + ZAHAR("zahar", YandexLanguage.RUSSIAN), + LEA("lea", YandexLanguage.GERMANY), + JOHN("john", YandexLanguage.ENGLISH), + AMIRA("amira", YandexLanguage.KAZAKH), + MADI("madi", YandexLanguage.RUSSIAN); + + private final String voice; + private final YandexLanguage lang; + + YandexVoice(String voice, YandexLanguage lang) { + this.lang = lang; + this.voice = voice; + } + + /** + *Возвращает язык для синтеза речи
+ * @return объектYandexLanguage, содержащий язык для синтеза речи
+ */
+ public YandexLanguage getLang() {
+ return lang;
+ }
+
+ /**
+ * Возвращает название голоса для синтеза речи
+ * @return название голоса + */ + public String getVoice() { + return voice; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/VoiceAssistantApplication.java b/src/main/java/com/iffomko/voiceAssistant/VoiceAssistantApplication.java index 4ca1014..69659d4 100644 --- a/src/main/java/com/iffomko/voiceAssistant/VoiceAssistantApplication.java +++ b/src/main/java/com/iffomko/voiceAssistant/VoiceAssistantApplication.java @@ -6,8 +6,8 @@ @SpringBootApplication public class VoiceAssistantApplication { - public static void main(String[] args) { - SpringApplication.run(VoiceAssistantApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(VoiceAssistantApplication.class, args); + } } diff --git a/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java b/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java new file mode 100644 index 0000000..32083dc --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/configs/SecurityConfig.java @@ -0,0 +1,64 @@ +package com.iffomko.voiceAssistant.configs; + +import com.iffomko.voiceAssistant.security.jwt.JwtConfigure; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + private final JwtConfigure jwtConfigure; + private final UserDetailsService userDetailsService; + + @Autowired + public SecurityConfig( + JwtConfigure jwtConfigure, + @Qualifier("userDetailsServiceDao") UserDetailsService userDetailsService + ) { + this.userDetailsService = userDetailsService; + this.jwtConfigure = jwtConfigure; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeHttpRequests() + .requestMatchers("/").permitAll() + .requestMatchers("/api/v1/auth/login").permitAll() + .requestMatchers("/api/v1/auth/register").permitAll() + .anyRequest().authenticated() + .and() + .apply(jwtConfigure); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); + } + + @Bean("authenticationManager") + public AuthenticationManager authenticationManager() { + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setPasswordEncoder(passwordEncoder()); + authenticationProvider.setUserDetailsService(userDetailsService); + + return new ProviderManager(authenticationProvider); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java b/src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java new file mode 100644 index 0000000..034db21 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/configs/VoiceAssistantConfig.java @@ -0,0 +1,33 @@ +package com.iffomko.voiceAssistant.configs; + +import com.iffomko.voiceAssistant.APIs.openAI.AIService; +import com.iffomko.voiceAssistant.APIs.speech.YandexClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan("com.iffomko.voiceAssistant") +public class VoiceAssistantConfig { + /** + * Создает бин клиента по работе с YandexAPI + * @return возвращает этот объект + */ + @Bean("yandexClient") + public YandexClient getYandexRecognition() { + String apiKey = System.getenv("API_KEY"); + + return new YandexClient(apiKey); + } + + /** + * Создает бин клиента по работе с OpenAI API + * @return возвращает этот объект + */ + @Bean("AIService") + public AIService getAIService() { + String apiKey = System.getenv("OPEN_AI_API_KEY"); + + return new AIService(apiKey); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java b/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java new file mode 100644 index 0000000..ce543ec --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/AnswerController.java @@ -0,0 +1,80 @@ +package com.iffomko.voiceAssistant.controllers; + +import com.iffomko.voiceAssistant.APIs.speech.types.YandexFormat; +import com.iffomko.voiceAssistant.controllers.errors.AnswerError; +import com.iffomko.voiceAssistant.controllers.data.AnswerRequestDTO; +import com.iffomko.voiceAssistant.controllers.data.AnswerResponseDTO; +import com.iffomko.voiceAssistant.controllers.services.AnswerService; +import com.iffomko.voiceAssistant.security.jwt.JwtTokenProvider; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/answer") +public class AnswerController { + private final AnswerService answerService; + private final JwtTokenProvider jwtTokenProvider; + + @Autowired + public AnswerController( + @Qualifier("answerService") AnswerService answerService, + JwtTokenProvider jwtTokenProvider + ) { + this.answerService = answerService; + this.jwtTokenProvider = jwtTokenProvider; + } + + /** + *Контроллер, который отвечает за ответ на вопрос, который содержится в аудио
+ */ + @PostMapping(produces="application/json") + @PreAuthorize("hasAuthority('get:answer')") + public ResponseEntityUser)
+ * @param jwtTokenProvider объект JwtTokenProvider, в нем содержится вся основная логика по работе
+ * с JWT токеном
+ * @param passwordEncoder любой объект реализующий интерфейс PasswordEncoder
+ */
+ @Autowired
+ public AuthenticationController(
+ @Qualifier("authenticationManager") AuthenticationManager authenticationManager,
+ UserDao userDao,
+ JwtTokenProvider jwtTokenProvider,
+ PasswordEncoder passwordEncoder
+ ) {
+ this.authenticationManager = authenticationManager;
+ this.jwtTokenProvider = jwtTokenProvider;
+ this.userDao = userDao;
+ this.passwordEncoder = passwordEncoder;
+ }
+
+ /**
+ * Эндпоинт, который отвечает за авторизацию пользователя в системе
+ * @param body объект AuthenticationRequestDTO, который содержит данные пользователя для входа в систему
+ * @return ответ ResponseEntity
+ */
+ @PostMapping("/login")
+ public ResponseEntityRegistrationRequestDTO, который содержит данные для регистрации пользователя
+ * @return объект ResponseEntity ответа пользователю
+ */
+ @PostMapping("/register")
+ public ResponseEntityОбъект ошибки. Он сериализуется в JSON, а так же содержит в себе сообщение ошибки и код ошибки
+ */ +@Data +public class AnswerError { + private String message; + private int code; +} diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/errors/AuthenticationError.java b/src/main/java/com/iffomko/voiceAssistant/controllers/errors/AuthenticationError.java new file mode 100644 index 0000000..95ca8ed --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/errors/AuthenticationError.java @@ -0,0 +1,14 @@ +package com.iffomko.voiceAssistant.controllers.errors; + +import lombok.Data; + +@Data +public class AuthenticationError { + private String message; + + public AuthenticationError() {} + + public AuthenticationError(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java b/src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java new file mode 100644 index 0000000..c1b3d15 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/controllers/services/AnswerService.java @@ -0,0 +1,140 @@ +package com.iffomko.voiceAssistant.controllers.services; + +import com.iffomko.voiceAssistant.APIs.openAI.AIService; +import com.iffomko.voiceAssistant.APIs.openAI.data.ModelResponse; +import com.iffomko.voiceAssistant.APIs.openAI.data.OpenAIChoices; +import com.iffomko.voiceAssistant.APIs.openAI.types.OpenAIRole; +import com.iffomko.voiceAssistant.APIs.speech.YandexClient; +import com.iffomko.voiceAssistant.APIs.speech.data.RecognitionResponse; +import com.iffomko.voiceAssistant.APIs.speech.types.YandexVoice; +import com.iffomko.voiceAssistant.db.entities.User; +import com.iffomko.voiceAssistant.db.services.UserService; +import com.iffomko.voiceAssistant.security.jwt.JwtTokenProvider; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +import java.util.Base64; + +/** + *Класс бизнес-логики формирования ответа на вопрос
+ */ +@Service +@Component +@Slf4j +@Scope("prototype") +public class AnswerService { + private final YandexClient yandexClient; + private final AIService chatGPT; + private final UserService userService; + + @Autowired + public AnswerService( + @Qualifier("yandexClient") YandexClient yandexClient, + @Qualifier("AIService") AIService chatGPT, + @Qualifier("UserDAO") UserService userService + ) { + this.yandexClient = yandexClient; + this.chatGPT = chatGPT; + this.userService = userService; + } + + /** + *В параметрах передается звук в формате Base64 и на выходе возвращается текстовое содержание этого звука
+ * @param voice - звук в кодировке base64 + * @return - возвращается текст звука илиnull, если звук не закодирован в Base64
+ */
+ private String audioToText(String voice, String format, Boolean profanityFilter) {
+ byte[] bytes = null;
+
+ try {
+ bytes = Base64.getDecoder().decode(voice);
+ } catch (IllegalArgumentException e) {
+ log.error(e.getMessage());
+ return null;
+ }
+
+ if (format != null) {
+ yandexClient.getRecognition().setFormat(format);
+ }
+
+ if (profanityFilter != null) {
+ yandexClient.getRecognition().setProfanityFilter(profanityFilter);
+ }
+
+ RecognitionResponse response = yandexClient.recognise(bytes);
+
+ if (response == null) {
+ return null;
+ }
+
+ return yandexClient.recognise(bytes).getResult();
+ }
+
+ /**
+ * Переводит текст в звук
+ * @param text - текст, который нужно озвучить + * @param format - формат выходного звука (mp3, oggopus, lpcm) + * @return - звук, закодированный в Base64 + */ + private String textToVoice(String text, String format) { + if (format != null) { + yandexClient.getSynthesis().setFormat(format); + } + + yandexClient.getSynthesis().setVoice(YandexVoice.FILIPP); + + byte[] voice = yandexClient.synthesis(text); + + if (voice == null) { + return null; + } + + return Base64.getEncoder().encodeToString(voice); + } + + /** + *Формирует ответ на вопрос пользователя
+ * @param username username текущего пользователя + * @param voice аудио-сообщение, в котором содержится вопрос + * @param format формат аудио-сообщения + * @param profanityFilter фильтр + * @return возвращает ответ на вопрос пользователя в виде аудиосообщения + */ + public String getAnswer( + @NonNull String username, + @NonNull String voice, + @NonNull String format, + @NonNull Boolean profanityFilter + ) { + User user = userService.findUserByUsername(username); + + if (user == null) { + return null; + } + + String text = audioToText(voice, format, profanityFilter); + ModelResponse response = chatGPT.ask(user.getId(), text); + + if (response == null) { + return null; + } + + StringBuilder answerBuilder = new StringBuilder(); + + for (OpenAIChoices choice : response.getChoices()) { + if (choice.getMessage().getRole().equals(OpenAIRole.ASSISTANT.getRole())) { + answerBuilder.append(choice.getMessage().getContent()); + answerBuilder.append("\n\n"); + } + } + + String answerChatGPT = answerBuilder.toString(); + + return textToVoice(answerChatGPT, format); + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java new file mode 100644 index 0000000..3beaaad --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/Permissions.java @@ -0,0 +1,25 @@ +package com.iffomko.voiceAssistant.db.entities; + +/** + * Перечисления всех прав + */ +public enum Permissions { + /** + * Право на получения ответа + */ + GET_ANSWER("get:answer"); + + private final String permission; + + Permissions(String permission) { + this.permission = permission; + } + + /** + * Возвращает право + * @return право + */ + public String getPermission() { + return permission; + } +} diff --git a/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java b/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java new file mode 100644 index 0000000..aaea7b0 --- /dev/null +++ b/src/main/java/com/iffomko/voiceAssistant/db/entities/Role.java @@ -0,0 +1,38 @@ +package com.iffomko.voiceAssistant.db.entities; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.List; +import java.util.Set; + +/** + * Перечисление всех ролей + */ +public enum Role { + /** + * Роль пользователя + */ + USER(Set.of(Permissions.GET_ANSWER)); + + private final SetUser
+ */
+ User getUserById(int id);
+
+ /**
+ * Возвращает пользователя по его username
+ * @param username username пользователя
+ * @return объект типа User
+ */
+ User findUserByUsername(String username);
+
+ /**
+ * Возвращает список всех пользователей
+ */
+ ListUserService, которая умеет работать с пользователями (User)
+ */
+@Service("UserDAO")
+@Transactional
+public class UserDao implements UserService {
+ private final UserRepository repository;
+
+ @Autowired
+ public UserDao(UserRepository repository) {
+ this.repository = repository;
+ }
+
+ /**
+ * Добавляет нового пользователя в систему
+ * @param user пользователь
+ * @throws IllegalArgumentException если пользователь уже существует
+ */
+ @Override
+ public void addUser(User user) throws IllegalArgumentException {
+ if (repository.existsUserByUsername(user.getUsername())) {
+ throw new IllegalArgumentException("Такое имя пользователя уже существует");
+ }
+
+ repository.save(user);
+ }
+
+ /**
+ * Удаляет существующего пользователя из системы по его id
+ * @param id уникальный идентификатор пользователя
+ * @return возвращает либо true в случае успешного удаления, либо false в случае неудачи
+ */
+ @Override
+ public boolean deleteUserById(int id) {
+ if (!repository.existsById(id)) {
+ return false;
+ }
+
+ repository.deleteById(id);
+
+ return true;
+ }
+
+ /**
+ * Возвращает существующего пользователя по его id или возвращает null
+ * @param id уникальный идентификатор пользователя
+ * @return объект типа User
+ */
+ @Override
+ public User getUserById(int id) {
+ return repository.findById(id).orElse(null);
+ }
+
+ /**
+ * Возвращает пользователя по его username или возвращает null
+ * @param username username пользователя
+ * @return объект типа User
+ */
+ @Override
+ public User findUserByUsername(String username) {
+ return repository.findByUsername(username).orElse(null);
+ }
+
+ /**
+ * Возвращает список всех пользователей
+ */
+ @Override
+ public ListUser в класс SecurityUser
+ * @param user сущность из базы данных
+ * @return объект SecurityUser
+ */
+ public static SecurityUser fromUser(User user) {
+ return new SecurityUser(
+ user.getUsername(),
+ user.getPassword(),
+ user.getRole().getAuthorities()
+ );
+ }
+}
diff --git a/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceDao.java b/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceDao.java
new file mode 100644
index 0000000..5613e93
--- /dev/null
+++ b/src/main/java/com/iffomko/voiceAssistant/security/UserDetailsServiceDao.java
@@ -0,0 +1,40 @@
+package com.iffomko.voiceAssistant.security;
+
+import com.iffomko.voiceAssistant.db.entities.User;
+import com.iffomko.voiceAssistant.db.services.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+/**
+ * Реализует интерфейс для взаимодействия с UserDetails и умеет подгружать пользователя по его username
+ */
+@Service("userDetailsServiceDao")
+public class UserDetailsServiceDao implements UserDetailsService {
+ private final UserService userService;
+
+ @Autowired
+ public UserDetailsServiceDao(@Qualifier("UserDAO") UserService userService) {
+ this.userService = userService;
+ }
+
+ /**
+ * Подгружает пользователя из базы данных по его username
+ * @param username username пользователя
+ * @return возвращает найденного пользователя (UserDetails)
+ * @throws UsernameNotFoundException выбрасывается если пользователь не существует
+ */
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ User user = userService.findUserByUsername(username);
+
+ if (user == null) {
+ throw new UsernameNotFoundException("User not found");
+ }
+
+ return SecurityUser.fromUser(user);
+ }
+}
diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java
new file mode 100644
index 0000000..80e1f6b
--- /dev/null
+++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtAuthenticationException.java
@@ -0,0 +1,32 @@
+package com.iffomko.voiceAssistant.security.jwt;
+
+
+import org.springframework.http.HttpStatus;
+
+/**
+ * Объект ошибки, который используется по работе с JWT токеном
+ */
+public class JwtAuthenticationException extends RuntimeException {
+ private final String message;
+ private final HttpStatus status;
+
+ public JwtAuthenticationException(String message, HttpStatus status) {
+ this.message = message;
+ this.status = status;
+ }
+
+ /**
+ * Возвращает сообщение ошибки
+ */
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Возвращает статус ошибки
+ */
+ public HttpStatus getStatus() {
+ return status;
+ }
+}
diff --git a/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java
new file mode 100644
index 0000000..3489395
--- /dev/null
+++ b/src/main/java/com/iffomko/voiceAssistant/security/jwt/JwtConfigure.java
@@ -0,0 +1,29 @@
+package com.iffomko.voiceAssistant.security.jwt;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.stereotype.Component;
+
+/**
+ * Конфигурация для фильтров JWT токена
+ */
+@Component
+public class JwtConfigure extends SecurityConfigurerAdapter