diff --git a/backend/settings.gradle b/backend/settings.gradle index 479b064..e355154 100644 --- a/backend/settings.gradle +++ b/backend/settings.gradle @@ -1,3 +1,4 @@ rootProject.name = 'backend' include 'smartjam-api' -include 'smartjam-analyzer' \ No newline at end of file +include 'smartjam-analyzer' +include 'smartjam-common' \ No newline at end of file diff --git a/backend/smartjam-analyzer/build.gradle b/backend/smartjam-analyzer/build.gradle index f119607..61e3a3c 100644 --- a/backend/smartjam-analyzer/build.gradle +++ b/backend/smartjam-analyzer/build.gradle @@ -38,4 +38,7 @@ dependencies { implementation 'be.tarsos.dsp:core:2.5' implementation 'be.tarsos.dsp:jvm:2.5' + + implementation project(':smartjam-common') + } \ No newline at end of file diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/api/kafka/S3StorageListener.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/api/kafka/S3StorageListener.java index 5945616..92141dc 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/api/kafka/S3StorageListener.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/api/kafka/S3StorageListener.java @@ -3,7 +3,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import com.smartjam.smartjamanalyzer.api.kafka.dto.S3EventDto; +import com.smartjam.common.dto.s3.S3EventDto; import com.smartjam.smartjamanalyzer.application.AudioAnalysisUseCase; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -32,9 +32,7 @@ public class S3StorageListener { topics = "s3-events", groupId = "smartjam-analyzer-group", concurrency = "3", - properties = { - "spring.json.value.default.type=com.smartjam.smartjamanalyzer.api" + ".kafka.dto" + ".S3EventDto" - }) + properties = {"spring.json.value.default.type=com.smartjam.common.dto.s3.S3EventDto"}) public void onFileUploaded(S3EventDto event, Acknowledgment ack) { if (event == null || event.records() == null || event.records().isEmpty()) { if (ack != null) ack.acknowledge(); diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/model/AnalysisResult.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/model/AnalysisResult.java index ab67e58..655245d 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/model/AnalysisResult.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/model/AnalysisResult.java @@ -2,6 +2,8 @@ import java.util.List; +import com.smartjam.common.dto.FeedbackEvent; + /** * Encapsulates the results of a performance evaluation, including overall scores, time-aligned feedback events, and * internal evaluation artifacts (like warping paths). @@ -41,12 +43,4 @@ private static double[][] copyMatrix(double[][] src) { public boolean isPassed() { return totalScore > 80; } - - public record FeedbackEvent( - double teacherStartTime, - double teacherEndTime, - double studentStartTime, - double studentEndTime, - String message, - double severity) {} } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/service/DtwPerformanceEvaluator.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/service/DtwPerformanceEvaluator.java index 5aac14e..180d190 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/service/DtwPerformanceEvaluator.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/service/DtwPerformanceEvaluator.java @@ -4,6 +4,8 @@ import java.util.Collections; import java.util.List; +import com.smartjam.common.dto.FeedbackEvent; +import com.smartjam.common.model.FeedbackType; import com.smartjam.smartjamanalyzer.domain.model.AnalysisResult; import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; import com.smartjam.smartjamanalyzer.domain.port.PerformanceEvaluator; @@ -37,20 +39,17 @@ public AnalysisResult evaluate(FeatureSequence reference, FeatureSequence studen double[][] dtwMatrix = computeCostMatrix(reference.frames(), student.frames()); List path = findWarpingPath(dtwMatrix); - List feedbacks = new ArrayList<>(); + List feedbacks = new ArrayList<>(); PathMetrics metrics = analyzePath(path, reference, student, feedbacks); return buildFinalResult(metrics, dtwMatrix, path, feedbacks); } private PathMetrics analyzePath( - List path, - FeatureSequence reference, - FeatureSequence student, - List feedbacks) { + List path, FeatureSequence reference, FeatureSequence student, List feedbacks) { - ErrorState pitchES = new ErrorState("Wrong note", reference.frameRate(), student.frameRate()); - ErrorState rhythmES = new ErrorState("Wrong rhythm", reference.frameRate(), student.frameRate()); + ErrorState pitchES = new ErrorState(FeedbackType.WRONG_NOTE, reference.frameRate(), student.frameRate()); + ErrorState rhythmES = new ErrorState(FeedbackType.WRONG_RHYTHM, reference.frameRate(), student.frameRate()); double totalPitchDist = 0; double totalRhythmDrift = 0; @@ -86,7 +85,7 @@ private PathMetrics analyzePath( private void processErrorState( ErrorState state, - List feedbacks, + List feedbacks, double tRef, double tStud, double severity, @@ -97,15 +96,15 @@ private void processErrorState( } } - private void flushIfActive(ErrorState state, List feedbacks) { + private void flushIfActive(ErrorState state, List feedbacks) { if (state.isActive()) { - AnalysisResult.FeedbackEvent ev = state.flush(); + FeedbackEvent ev = state.flush(); if (ev != null) feedbacks.add(ev); } } private AnalysisResult buildFinalResult( - PathMetrics metrics, double[][] matrix, List path, List feedbacks) { + PathMetrics metrics, double[][] matrix, List path, List feedbacks) { double avgPitchError = metrics.totalPitchDist / path.size(); double pScore = Math.max(0.0, 100.0 * (1.0 - Math.pow(avgPitchError / 0.5, 2))); @@ -188,7 +187,7 @@ List findWarpingPath(double[][] dtw) { private record PathMetrics(double totalPitchDist, double totalRhythmDrift) {} private static class ErrorState { - private final String message; + private final FeedbackType type; private final double refFrameDuration; private final double studFrameDuration; private double rStart = -1; @@ -198,8 +197,8 @@ private static class ErrorState { private double maxSev = 0; private int graceCounter = 0; - ErrorState(String message, float refFrameRate, float studFrameRate) { - this.message = message; + ErrorState(FeedbackType type, float refFrameRate, float studFrameRate) { + this.type = type; this.refFrameDuration = 1.0 / refFrameRate; this.studFrameDuration = 1.0 / studFrameRate; } @@ -230,7 +229,7 @@ boolean isActive() { return rStart >= 0; } - AnalysisResult.FeedbackEvent flush() { + FeedbackEvent flush() { double refDuration = (rEnd - rStart) + refFrameDuration; double studDuration = (sEnd - sStart) + studFrameDuration; @@ -239,8 +238,8 @@ AnalysisResult.FeedbackEvent flush() { return null; } - AnalysisResult.FeedbackEvent event = new AnalysisResult.FeedbackEvent( - rStart, rEnd + refFrameDuration, sStart, sEnd + studFrameDuration, message, maxSev); + FeedbackEvent event = + new FeedbackEvent(rStart, rEnd + refFrameDuration, sStart, sEnd + studFrameDuration, type, maxSev); reset(); return event; } diff --git a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/api/listener/S3StorageListenerTest.java b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/api/listener/S3StorageListenerTest.java index a52b4d0..9e68f5f 100644 --- a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/api/listener/S3StorageListenerTest.java +++ b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/api/listener/S3StorageListenerTest.java @@ -3,8 +3,8 @@ import java.util.Collections; import java.util.List; +import com.smartjam.common.dto.s3.S3EventDto; import com.smartjam.smartjamanalyzer.api.kafka.S3StorageListener; -import com.smartjam.smartjamanalyzer.api.kafka.dto.S3EventDto; import com.smartjam.smartjamanalyzer.application.AudioAnalysisUseCase; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisIntegrationTest.java b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisIntegrationTest.java index 0f38f3f..9df5e55 100644 --- a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisIntegrationTest.java +++ b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisIntegrationTest.java @@ -66,7 +66,7 @@ void shouldPerformFullAnalysisCycle() throws Exception { String feedbackReport = result.feedback().stream() .map(e -> String.format( "-> [%s] с %5.2fs до %5.2fs (Ученик: с %5.2fs до %5.2fs) | Тяжесть: %.2f", - e.message(), + e.type(), e.teacherStartTime(), e.teacherEndTime(), e.studentStartTime(), diff --git a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/domain/service/DtwPerformanceEvaluatorTest.java b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/domain/service/DtwPerformanceEvaluatorTest.java index c491102..4beaca4 100644 --- a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/domain/service/DtwPerformanceEvaluatorTest.java +++ b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/domain/service/DtwPerformanceEvaluatorTest.java @@ -4,6 +4,8 @@ import java.util.Collections; import java.util.List; +import com.smartjam.common.dto.FeedbackEvent; +import com.smartjam.common.model.FeedbackType; import com.smartjam.smartjamanalyzer.domain.model.AnalysisResult; import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; import org.junit.jupiter.api.DisplayName; @@ -103,7 +105,7 @@ void shouldPenalizeEarlyTermination() { assertTrue(result.totalScore() < 50.0, "Score должен быть низким при обрыве записи"); boolean hasRhythmError = - result.feedback().stream().anyMatch(e -> e.message().equals("Wrong rhythm")); + result.feedback().stream().anyMatch(e -> e.type().equals(FeedbackType.WRONG_RHYTHM)); assertTrue(hasRhythmError); } @@ -128,8 +130,8 @@ void shouldMergeFrequentShortErrorsRealistic() { AnalysisResult result = evaluator.evaluate(new FeatureSequence(ref, 20f), new FeatureSequence(stud, 20f)); - List pitchEvents = result.feedback().stream() - .filter(e -> e.message().equals("Wrong note")) + List pitchEvents = result.feedback().stream() + .filter(e -> e.type().equals(FeedbackType.WRONG_NOTE)) .toList(); assertTrue(pitchEvents.size() < 5, "Ошибки должны склеиваться, лог не должен спамить"); } @@ -149,7 +151,7 @@ void shouldBeSensitiveToSmallPitchShiftsRealistic() { result.pitchScore() > 20.0 && result.pitchScore() < 80.0, "Оценка должна быть промежуточной при реалистичных признаках"); boolean hasPitchError = - result.feedback().stream().anyMatch(e -> e.message().equals("Wrong note")); + result.feedback().stream().anyMatch(e -> e.type().equals(FeedbackType.WRONG_NOTE)); assertTrue(hasPitchError); } @@ -166,7 +168,7 @@ void severityShouldNotBeMaxForSmallError() { AnalysisResult result = evaluator.evaluate(new FeatureSequence(ref, 20f), new FeatureSequence(stud, 20f)); result.feedback().stream() - .filter(e -> e.message().equals("Wrong note")) + .filter(e -> e.type().equals(FeedbackType.WRONG_NOTE)) .forEach(e -> assertTrue(e.severity() < 0.9, "Severity должна быть <0.9")); } @@ -206,11 +208,11 @@ void shortGapsShouldMergeRealistic() { AnalysisResult result = evaluator.evaluate(refSeq, studSeq); - List pitchEvents = result.feedback().stream() - .filter(e -> e.message().equals("Wrong note")) + List pitchEvents = result.feedback().stream() + .filter(e -> e.type().equals(FeedbackType.WRONG_NOTE)) .toList(); assertEquals(1, pitchEvents.size(), "Ошибки должны склеиться в одно событие"); - AnalysisResult.FeedbackEvent event = pitchEvents.getFirst(); + FeedbackEvent event = pitchEvents.getFirst(); assertTrue(event.studentEndTime() - event.studentStartTime() > 2.0); } @@ -242,9 +244,9 @@ void longGapShouldSplitRealistic() { AnalysisResult result = evaluator.evaluate(refSeq, studSeq); - List pitchEvents; + List pitchEvents; pitchEvents = result.feedback().stream() - .filter(e -> e.message().equals("Wrong note")) + .filter(e -> e.type().equals(FeedbackType.WRONG_NOTE)) .toList(); assertEquals(2, pitchEvents.size(), "Длинный перерыв должен разделить ошибки"); } diff --git a/backend/smartjam-api/build.gradle b/backend/smartjam-api/build.gradle index 3fbe05b..e49f2f8 100644 --- a/backend/smartjam-api/build.gradle +++ b/backend/smartjam-api/build.gradle @@ -9,4 +9,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2' + + + implementation project(':smartjam-common') } \ No newline at end of file diff --git a/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/infrastructure/openapi/OpenApiConfig.java b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/infrastructure/openapi/OpenApiConfig.java new file mode 100644 index 0000000..66288b4 --- /dev/null +++ b/backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/infrastructure/openapi/OpenApiConfig.java @@ -0,0 +1,62 @@ +package com.smartjam.smartjamapi.infrastructure.openapi; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for OpenAPI documentation using SpringDoc. Defines metadata, security schemes, and global requirements + * for the API. + */ +@Configuration +public class OpenApiConfig { + + private static final String SECURITY_SCHEME_NAME = "bearerAuth"; + /** + * Customizes the OpenAPI definition. + * + * @return a fully configured OpenAPI object. + */ + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(createApiInfo()) + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) + .components(new Components().addSecuritySchemes(SECURITY_SCHEME_NAME, createSecurityScheme())); + } + + private Info createApiInfo() { + return new Info() + .title("SmartJam API") + .version("1.0") + .description(""" + Interactive Music Learning and Performance Analysis System. + + Development Team: + - Sanjar Satlykov ([satlykovs@gmail.com](mailto:satlykovs@gmail.com)) + - Anton Podrezov ([toni.podrezov@gmail.com](mailto:toni.podrezov@gmail.com)) + - Serj Baskov ([baskovs450@gmail.com](mailto:baskovs450@gmail.com)) + + Supervised by: + - Andrey Sheremeev ([sheremeev.andrey@gmail.com](mailto:sheremeev.andrey@gmail.com)) + """) + .contact(new Contact() + .name("SmartJam Team") + .email("satlykovs@gmail.com") + .url("https://github.com/Satlykovs/SmartJam")); + } + + private SecurityScheme createSecurityScheme() { + return new SecurityScheme() + .name(SECURITY_SCHEME_NAME) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description("Provide your JWT token obtained from the login endpoint."); + } +} diff --git a/backend/smartjam-api/src/main/resources/application.yaml b/backend/smartjam-api/src/main/resources/application.yaml index 16aca50..4064ef2 100644 Binary files a/backend/smartjam-api/src/main/resources/application.yaml and b/backend/smartjam-api/src/main/resources/application.yaml differ diff --git a/backend/smartjam-common/build.gradle b/backend/smartjam-common/build.gradle new file mode 100644 index 0000000..73baefe --- /dev/null +++ b/backend/smartjam-common/build.gradle @@ -0,0 +1,20 @@ + +plugins { + id 'java-library' +} + +bootJar { + enabled = false +} + +jar { + enabled = true +} + +dependencies { + api 'com.fasterxml.jackson.core:jackson-annotations' + api 'com.fasterxml.jackson.core:jackson-databind' +} + +group = 'com.smartjam' +version = '0.0.1-SNAPSHOT' diff --git a/backend/smartjam-common/src/main/java/com/smartjam/common/dto/FeedbackEvent.java b/backend/smartjam-common/src/main/java/com/smartjam/common/dto/FeedbackEvent.java new file mode 100644 index 0000000..e80018a --- /dev/null +++ b/backend/smartjam-common/src/main/java/com/smartjam/common/dto/FeedbackEvent.java @@ -0,0 +1,15 @@ +package com.smartjam.common.dto; + +import com.smartjam.common.model.FeedbackType; + +/** + * A shared contract for a single feedback occurrence. Used by Analyzer to produce results and by API/Mobile to display + * them. + */ +public record FeedbackEvent( + double teacherStartTime, + double teacherEndTime, + double studentStartTime, + double studentEndTime, + FeedbackType type, + double severity) {} diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/api/kafka/dto/S3EventDto.java b/backend/smartjam-common/src/main/java/com/smartjam/common/dto/s3/S3EventDto.java similarity index 96% rename from backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/api/kafka/dto/S3EventDto.java rename to backend/smartjam-common/src/main/java/com/smartjam/common/dto/s3/S3EventDto.java index a12b282..2ffa0d9 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/api/kafka/dto/S3EventDto.java +++ b/backend/smartjam-common/src/main/java/com/smartjam/common/dto/s3/S3EventDto.java @@ -1,4 +1,4 @@ -package com.smartjam.smartjamanalyzer.api.kafka.dto; +package com.smartjam.common.dto.s3; import java.util.List; diff --git a/backend/smartjam-common/src/main/java/com/smartjam/common/model/AudioProcessingStatus.java b/backend/smartjam-common/src/main/java/com/smartjam/common/model/AudioProcessingStatus.java new file mode 100644 index 0000000..ebd5fc7 --- /dev/null +++ b/backend/smartjam-common/src/main/java/com/smartjam/common/model/AudioProcessingStatus.java @@ -0,0 +1,13 @@ +package com.smartjam.common.model; + +/** + * Represents the lifecycle stages of an audio processing task. Used to track the state from the moment a database + * record is created until the final analysis result is stored. + */ +public enum AudioProcessingStatus { + AWAITING_UPLOAD, + UPLOADED, + ANALYZING, + COMPLETED, + FAILED +} diff --git a/backend/smartjam-common/src/main/java/com/smartjam/common/model/FeedbackType.java b/backend/smartjam-common/src/main/java/com/smartjam/common/model/FeedbackType.java new file mode 100644 index 0000000..5e5586f --- /dev/null +++ b/backend/smartjam-common/src/main/java/com/smartjam/common/model/FeedbackType.java @@ -0,0 +1,10 @@ +package com.smartjam.common.model; + +/** + * Categories of musical discrepancies identified during the comparison of a student's performance against the teacher's + * reference. + */ +public enum FeedbackType { + WRONG_NOTE, + WRONG_RHYTHM +} diff --git a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/api/kafka/dto/S3EventDtoTest.java b/backend/smartjam-common/src/test/java/common/dto/s3/S3EventDtoTest.java similarity index 91% rename from backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/api/kafka/dto/S3EventDtoTest.java rename to backend/smartjam-common/src/test/java/common/dto/s3/S3EventDtoTest.java index 72e6036..18e70c3 100644 --- a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/api/kafka/dto/S3EventDtoTest.java +++ b/backend/smartjam-common/src/test/java/common/dto/s3/S3EventDtoTest.java @@ -1,9 +1,10 @@ -package com.smartjam.smartjamanalyzer.api.kafka.dto; +package common.dto.s3; import com.fasterxml.jackson.databind.ObjectMapper; +import com.smartjam.common.dto.s3.S3EventDto; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class S3EventDtoTest { private final ObjectMapper objectMapper = new ObjectMapper();