From 354c1bf9b5511b5bc4ff33c74afb7adc8e881d32 Mon Sep 17 00:00:00 2001 From: Satlykovs Date: Sat, 21 Mar 2026 02:11:41 +0300 Subject: [PATCH 1/4] feat: implement binary serializer with tests and define repository ports --- .../domain/port/ReferenceRepository.java | 18 +- .../utils/FeatureBinarySerializer.java | 107 +++++++++ .../converter/FfmpegAudioConverterTest.java | 2 +- .../utils/FeatureBinarySerializerTest.java | 210 ++++++++++++++++++ 4 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java create mode 100644 backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java index 35d07bb..eec6813 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java @@ -4,8 +4,22 @@ import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; +/** Domain port for managing teacher reference features. */ public interface ReferenceRepository { - void save(UUID targetId, FeatureSequence features); - FeatureSequence findById(UUID targetId); + /** + * Saves the spectral features of a teacher's reference track. + * + * @param assignmentId Unique identifier of the assignment. + * @param features Extracted features to persist. + */ + void save(UUID assignmentId, FeatureSequence features); + + /** + * Retrieves reference features for comparison. + * + * @param assignmentId Unique identifier of the assignment. + * @return The feature sequence or null if not found. + */ + FeatureSequence findById(UUID assignmentId); } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java new file mode 100644 index 0000000..e32c756 --- /dev/null +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java @@ -0,0 +1,107 @@ +package com.smartjam.smartjamanalyzer.infrastructure.utils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** Binary serializer for spectral feature matrices. Storage format: [int:N frames][int:M bins][N * M raw floats] */ +public class FeatureBinarySerializer { + + private static final int HEADER_SIZE = 8; + + private FeatureBinarySerializer() { + // utility class, no need to be constructed + } + + /** + * Serializes a list of equally sized float frames into a byte array. + * + * @param frames list of frames (each frame is an array of floats) + * @return byte array with little-endian representation + * @throws IllegalArgumentException if frames are null, empty, or frames have different lengths + */ + public static byte[] serialize(List frames) { + + Objects.requireNonNull(frames, "frames list must not be null"); + + if (frames.isEmpty()) { + return new byte[0]; + } + + int frameCount = frames.size(); + int binCount = frames.getFirst().length; + + for (var frame : frames) { + if (frame.length != binCount) { + throw new IllegalArgumentException("All frames must have the same number of bins. Expected " + binCount + + ", but got " + frame.length); + } + } + + long totalElements = (long) frameCount * binCount; + if (totalElements > Integer.MAX_VALUE / Float.BYTES) { + throw new IllegalArgumentException("Too many elements to serialize: " + totalElements); + } + + int totalBytes = HEADER_SIZE + frameCount * binCount * Float.BYTES; + + ByteBuffer buffer = ByteBuffer.allocate(totalBytes).order(ByteOrder.LITTLE_ENDIAN); + + buffer.putInt(frameCount); + buffer.putInt(binCount); + + for (float[] frame : frames) { + for (float val : frame) { + buffer.putFloat(val); + } + } + + return buffer.array(); + } + + /** + * Deserializes a byte array back to a list of frames. + * + * @param data byte array produced by {@link #serialize(List)} + * @return list of frames, or empty list if data is null or too short + * @throws IllegalArgumentException if the data is malformed (e.g., negative sizes, insufficient length) + */ + public static List deserialize(byte[] data) { + if (data == null || data.length < HEADER_SIZE) { + return new ArrayList<>(); + } + + ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); + + int frameCount = buffer.getInt(); + int binCount = buffer.getInt(); + + if (frameCount < 0 || binCount < 0) { + throw new IllegalArgumentException("Invalid header: frameCount or binCount is negative"); + } + + long totalElements = (long) frameCount * binCount; + if (totalElements > Integer.MAX_VALUE / Float.BYTES) { + throw new IllegalArgumentException("Too many elements to deserialize: " + totalElements); + } + + int expectedBytes = HEADER_SIZE + frameCount * binCount * Float.BYTES; + + if (data.length < expectedBytes) { + throw new IllegalArgumentException( + "Data too short. Expected " + expectedBytes + " bytes, got " + data.length); + } + + List frames = new ArrayList<>(frameCount); + for (int i = 0; i < frameCount; ++i) { + float[] frame = new float[binCount]; + for (int j = 0; j < binCount; ++j) { + frame[j] = buffer.getFloat(); + } + frames.add(frame); + } + return frames; + } +} diff --git a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/converter/FfmpegAudioConverterTest.java b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/converter/FfmpegAudioConverterTest.java index 09fd272..6561f49 100644 --- a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/converter/FfmpegAudioConverterTest.java +++ b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/converter/FfmpegAudioConverterTest.java @@ -17,7 +17,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; -// NOTE: добавить прокидывание фильтра сюда +// TODO: добавить прокидывание фильтра сюда @ExtendWith(MockitoExtension.class) class FfmpegAudioConverterTest { diff --git a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java new file mode 100644 index 0000000..208bc86 --- /dev/null +++ b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java @@ -0,0 +1,210 @@ +package com.smartjam.smartjamanalyzer.infrastructure.utils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class FeatureBinarySerializerTest { + + @Test + @DisplayName("Сериализация и десериализация сохраняет данные") + void shouldMaintainDataIntegrity() { + + float[] frame1 = {0.1f, 0.5f, 0.9f}; + float[] frame2 = {0.2f, 0.4f, 0.6f}; + List original = List.of(frame1, frame2); + + byte[] encoded = FeatureBinarySerializer.serialize(original); + assertNotNull(encoded); + assertTrue(encoded.length > 8); + + List decoded = FeatureBinarySerializer.deserialize(encoded); + + assertEquals(original.size(), decoded.size()); + assertEquals(original.getFirst().length, decoded.getFirst().length); + + for (int i = 0; i < original.size(); i++) { + assertArrayEquals(original.get(i), decoded.get(i), 1e-6f); + } + } + + @Test + @DisplayName("Пустой список возвращает пустой массив и пустой результат") + void emptyList() { + + List empty = List.of(); + byte[] encoded = FeatureBinarySerializer.serialize(empty); + + assertEquals(0, encoded.length); + + List decoded = FeatureBinarySerializer.deserialize(encoded); + + assertTrue(decoded.isEmpty()); + } + + @Test + @DisplayName("Сериализация с null вызывает NullPointerException") + void serializeNullThrows() { + + assertThrows(NullPointerException.class, () -> FeatureBinarySerializer.serialize(null)); + } + + @Test + @DisplayName("Десериализация null возвращает пустой список") + void deserializeNullReturnsEmpty() { + + List result = FeatureBinarySerializer.deserialize(null); + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Десериализация слишком короткого массива возвращает пустой список") + void deserializeTooShortReturnsEmpty() { + + byte[] shortData = new byte[4]; + List result = FeatureBinarySerializer.deserialize(shortData); + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Сериализация с фреймами разной длины вызывает исключение") + void serializeFramesWithDifferentLengthsThrows() { + + List frames = List.of(new float[] {1.0f, 2.0f}, new float[] {3.0f, 4.0f, 5.0f}); + assertThrows(IllegalArgumentException.class, () -> FeatureBinarySerializer.serialize(frames)); + } + + @Test + @DisplayName("Десериализация с отрицательным frameCount вызывает исключение") + void deserializeNegativeFrameCountThrows() { + + ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(-1); + buffer.putInt(10); + byte[] data = buffer.array(); + + assertThrows(IllegalArgumentException.class, () -> FeatureBinarySerializer.deserialize(data)); + } + + @Test + @DisplayName("Десериализация с отрицательным binCount вызывает исключение") + void deserializeNegativeBinCountThrows() { + + ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(5); + buffer.putInt(-3); + byte[] data = buffer.array(); + + assertThrows(IllegalArgumentException.class, () -> FeatureBinarySerializer.deserialize(data)); + } + + @Test + @DisplayName("Десериализация с недостаточным количеством данных вызывает исключение") + void deserializeInsufficientDataThrows() { + + ByteBuffer buffer = ByteBuffer.allocate(8 + Float.BYTES).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(1); + buffer.putInt(2); + buffer.putFloat(1.0f); + byte[] data = new byte[buffer.position()]; + System.arraycopy(buffer.array(), 0, data, 0, data.length); + + assertThrows(IllegalArgumentException.class, () -> FeatureBinarySerializer.deserialize(data)); + } + + @Test + @DisplayName("Проверка little-endian порядка байтов") + void littleEndianOrder() { + + List frames = List.of(new float[] {1.0f}); + byte[] encoded = FeatureBinarySerializer.serialize(frames); + + ByteBuffer buffer = ByteBuffer.wrap(encoded).order(ByteOrder.LITTLE_ENDIAN); + assertEquals(1, buffer.getInt()); + assertEquals(1, buffer.getInt()); + assertEquals(1.0f, buffer.getFloat(), 1e-6f); + } + + @Test + @DisplayName("Работа с большим количеством данных (3000 фреймов по 100 бинов)") + void largeData() { + + int framesCount = 3000; + int bins = 100; + + List frames = new ArrayList<>(framesCount); + + for (int i = 0; i < framesCount; i++) { + float[] frame = new float[bins]; + for (int j = 0; j < bins; j++) { + frame[j] = (float) (i + j); + } + frames.add(frame); + } + + byte[] encoded = FeatureBinarySerializer.serialize(frames); + List decoded = FeatureBinarySerializer.deserialize(encoded); + + assertEquals(framesCount, decoded.size()); + + for (int i = 0; i < framesCount; i++) { + assertArrayEquals(frames.get(i), decoded.get(i), 1e-6f); + } + } + + @Test + @DisplayName("Один фрейм с одним значением") + void singleFrameSingleValue() { + + List original = List.of(new float[] {42.0f}); + + byte[] encoded = FeatureBinarySerializer.serialize(original); + List decoded = FeatureBinarySerializer.deserialize(encoded); + + assertEquals(1, decoded.size()); + assertEquals(1, decoded.getFirst().length); + assertEquals(42.0f, decoded.getFirst()[0], 1e-6f); + } + + @Test + @DisplayName("Сериализация и десериализация с максимальными значениями float") + void extremeFloatValues() { + + List frames = List.of( + new float[] {Float.MAX_VALUE, -Float.MAX_VALUE, Float.MIN_VALUE, Float.NaN, Float.POSITIVE_INFINITY}); + + byte[] encoded = FeatureBinarySerializer.serialize(frames); + List decoded = FeatureBinarySerializer.deserialize(encoded); + + float[] originalFrame = frames.getFirst(); + float[] decodedFrame = decoded.getFirst(); + assertEquals(originalFrame.length, decodedFrame.length); + for (int i = 0; i < originalFrame.length; i++) { + if (Float.isNaN(originalFrame[i])) { + assertTrue(Float.isNaN(decodedFrame[i])); + } else { + assertEquals(originalFrame[i], decodedFrame[i], 1e-6f); + } + } + } + + @Test + @DisplayName("Защита от переполнения при десериализации") + void deserializeOverflowThrows() { + + ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + + int large = Integer.MAX_VALUE / 1000; + buffer.putInt(large); + buffer.putInt(large); + byte[] data = buffer.array(); + + assertThrows(IllegalArgumentException.class, () -> FeatureBinarySerializer.deserialize(data)); + } +} From fa52afe70780c7437913193a06aea2f6a542466c Mon Sep 17 00:00:00 2001 From: Satlykovs Date: Sun, 22 Mar 2026 02:17:10 +0300 Subject: [PATCH 2/4] feat: implement persistence layer with state management --- .../application/AudioAnalysisUseCase.java | 77 +++++++++++++-- .../domain/port/ReferenceRepository.java | 3 + .../domain/port/ResultRepository.java | 6 ++ .../adapter/AssignmentPersistenceAdapter.java | 51 ++++++++++ .../adapter/SubmissionPersistenceAdapter.java | 50 ++++++++++ .../persistence/entity/AssignmentEntity.java | 29 ++++++ .../persistence/entity/SubmissionEntity.java | 47 +++++++++ .../repository/JpaAssignmentRepository.java | 16 +++ .../repository/JpaSubmissionRepository.java | 16 +++ .../utils/FeatureBinarySerializer.java | 31 +++--- .../application/AudioAnalysisUseCaseTest.java | 64 +++++++++--- .../utils/FeatureBinarySerializerTest.java | 98 +++++++++---------- 12 files changed, 401 insertions(+), 87 deletions(-) create mode 100644 backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/AssignmentPersistenceAdapter.java create mode 100644 backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java create mode 100644 backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/AssignmentEntity.java create mode 100644 backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/SubmissionEntity.java create mode 100644 backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaAssignmentRepository.java create mode 100644 backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaSubmissionRepository.java diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java index 0974069..c6f47d1 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java @@ -1,7 +1,10 @@ package com.smartjam.smartjamanalyzer.application; import java.nio.file.Path; +import java.util.UUID; +import com.smartjam.common.model.AudioProcessingStatus; +import com.smartjam.smartjamanalyzer.domain.model.AnalysisResult; import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; import com.smartjam.smartjamanalyzer.domain.port.*; import lombok.RequiredArgsConstructor; @@ -19,12 +22,21 @@ public class AudioAnalysisUseCase { private final WorkspaceFactory workspaceFactory; private final FeatureExtractor featureExtractor; + private final PerformanceEvaluator performanceEvaluator; + private final ReferenceRepository referenceRepository; + private final ResultRepository resultRepository; + private final DebugVisualizer debugVisualizer; + public void execute(String bucket, String fileKey) { - try (Workspace workspace = workspaceFactory.create()) { + UUID entityId = extractUuid(fileKey); + + // TODO: Добавить обработку(проверку типа) входящего файла + + // TODO: Добавить нормальный сбор метрик - // TODO: Добавить обработку(проверку типа) входящего файла + try (Workspace workspace = workspaceFactory.create()) { + updateStatus(bucket, entityId, AudioProcessingStatus.ANALYZING, null); - // TODO: Добавить нормальный сбор метрик StopWatch watch = new StopWatch(fileKey); log.info("=== Начало обработки файла: {} из бакета {} ===", fileKey, bucket); @@ -37,24 +49,73 @@ public void execute(String bucket, String fileKey) { Path cleanWavFile = audioConverter.convertToStandardWav(localFile, workspace); watch.stop(); - watch.start("Business Logic (Math)"); - + watch.start("Feature Extraction"); FeatureSequence features = featureExtractor.extract(cleanWavFile); + watch.stop(); log.info("Extracted {} feature frames", features.frames().size()); + watch.start("Evaluation & Persistence"); if ("references".equals(bucket)) { - log.info("Действие: Обработка ЭТАЛОНА учителя..."); + handleTeacherReference(entityId, features); } else if ("submissions".equals(bucket)) { - log.info("Действие: Обработка ПОПЫТКИ ученика..."); + handleStudentSubmission(entityId, features); } watch.stop(); log.info("Результаты обработки {}: \n{}", fileKey, watch.prettyPrint()); } catch (Exception e) { - log.error("Ошибка в UseCase для файла {}: {}", fileKey, e.getMessage()); + log.error("Ошибка в UseCase для файла {}: {}", fileKey, e.getMessage(), e); + updateStatus(bucket, entityId, AudioProcessingStatus.FAILED, e.getMessage()); throw new RuntimeException("Business logic failed", e); } } + + private void updateStatus(String bucket, UUID id, AudioProcessingStatus status, String error) { + try { + if ("references".equals(bucket)) { + referenceRepository.updateStatus(id, status, error); + } else { + resultRepository.updateStatus(id, status, error); + } + } catch (Exception ex) { + log.error("Failed to update status in DB for {}: {}", id, ex.getMessage()); + } + } + + private void handleTeacherReference(UUID assignmentId, FeatureSequence teacherFeatures) { + log.info("Saving teacher reference features for assignment: {}", assignmentId); + referenceRepository.save(assignmentId, teacherFeatures); + } + + private void handleStudentSubmission(UUID submissionId, FeatureSequence studentFeatures) { + log.info("Evaluating student submission: {}", submissionId); + + UUID assignmentId = resultRepository.findAssignmentIdBySubmissionId(submissionId); + + FeatureSequence teacherFeatures = referenceRepository.findById(assignmentId); + + if (teacherFeatures == null) { + throw new IllegalStateException("Reference features not ready for assignment: " + assignmentId); + } + + AnalysisResult result = performanceEvaluator.evaluate(teacherFeatures, studentFeatures); + + resultRepository.save(submissionId, result); + + debugVisualizer.generateHeatmap(result, "debug_" + submissionId + ".png"); + + log.info("Submission {} evaluation completed.", submissionId); + } + + private UUID extractUuid(String fileKey) { + try { + String rawUuid = fileKey.contains(".") ? fileKey.substring(0, fileKey.lastIndexOf(".")) : fileKey; + + return UUID.fromString(rawUuid); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid S3 file key. Expected UUID, got: " + fileKey); + } + } } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java index eec6813..05a3862 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java @@ -2,6 +2,7 @@ import java.util.UUID; +import com.smartjam.common.model.AudioProcessingStatus; import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; /** Domain port for managing teacher reference features. */ @@ -22,4 +23,6 @@ public interface ReferenceRepository { * @return The feature sequence or null if not found. */ FeatureSequence findById(UUID assignmentId); + + void updateStatus(UUID id, AudioProcessingStatus status, String errorMessage); } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java index 9137d41..b252c84 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java @@ -2,8 +2,14 @@ import java.util.UUID; +import com.smartjam.common.model.AudioProcessingStatus; import com.smartjam.smartjamanalyzer.domain.model.AnalysisResult; public interface ResultRepository { + void save(UUID submissionId, AnalysisResult result); + + UUID findAssignmentIdBySubmissionId(UUID submissionId); + + void updateStatus(UUID id, AudioProcessingStatus status, String errorMessage); } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/AssignmentPersistenceAdapter.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/AssignmentPersistenceAdapter.java new file mode 100644 index 0000000..fc6694f --- /dev/null +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/AssignmentPersistenceAdapter.java @@ -0,0 +1,51 @@ +package com.smartjam.smartjamanalyzer.infrastructure.persistence.adapter; + +import java.util.UUID; + +import jakarta.transaction.Transactional; + +import com.smartjam.common.model.AudioProcessingStatus; +import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; +import com.smartjam.smartjamanalyzer.domain.port.ReferenceRepository; +import com.smartjam.smartjamanalyzer.infrastructure.persistence.entity.AssignmentEntity; +import com.smartjam.smartjamanalyzer.infrastructure.persistence.repository.JpaAssignmentRepository; +import com.smartjam.smartjamanalyzer.infrastructure.utils.FeatureBinarySerializer; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AssignmentPersistenceAdapter implements ReferenceRepository { + private final JpaAssignmentRepository repository; + + @Override + @Transactional + public void save(UUID assignmentId, FeatureSequence features) { + byte[] bytes = FeatureBinarySerializer.serialize(features); + + AssignmentEntity entity = repository + .findById(assignmentId) + .orElseThrow(() -> new IllegalStateException("Assignment metadata missing for ID: " + assignmentId + + ". It might have " + "been deleted or not created yet.")); + + entity.setReferenceSpectreCache(bytes); + entity.setStatus(AudioProcessingStatus.COMPLETED); + entity.setErrorMessage(null); + + repository.save(entity); + } + + @Override + public FeatureSequence findById(UUID assignmentId) { + return repository + .findById(assignmentId) + .map(e -> FeatureBinarySerializer.deserialize(e.getReferenceSpectreCache())) + .orElse(null); + } + + @Override + @Transactional + public void updateStatus(UUID id, AudioProcessingStatus status, String errorMessage) { + repository.updateStatus(id, status, errorMessage); + } +} diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java new file mode 100644 index 0000000..3070d8b --- /dev/null +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java @@ -0,0 +1,50 @@ +package com.smartjam.smartjamanalyzer.infrastructure.persistence.adapter; + +import java.util.UUID; + +import jakarta.transaction.Transactional; + +import com.smartjam.common.model.AudioProcessingStatus; +import com.smartjam.smartjamanalyzer.domain.model.AnalysisResult; +import com.smartjam.smartjamanalyzer.domain.port.ResultRepository; +import com.smartjam.smartjamanalyzer.infrastructure.persistence.entity.SubmissionEntity; +import com.smartjam.smartjamanalyzer.infrastructure.persistence.repository.JpaSubmissionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SubmissionPersistenceAdapter implements ResultRepository { + private final JpaSubmissionRepository repository; + + @Override + @Transactional + public void save(UUID submissionId, AnalysisResult result) { + SubmissionEntity entity = repository + .findById(submissionId) + .orElseThrow(() -> new IllegalStateException("Submission record missing for ID: " + submissionId)); + + entity.setTotalScore(result.totalScore()); + entity.setPitchScore(result.pitchScore()); + entity.setRhythmScore(result.rhythmScore()); + entity.setAnalysisFeedback(result.feedback()); + entity.setStatus(AudioProcessingStatus.COMPLETED); + entity.setErrorMessage(null); + + repository.save(entity); + } + + @Override + public UUID findAssignmentIdBySubmissionId(UUID submissionId) { + return repository + .findById(submissionId) + .map(SubmissionEntity::getAssignmentId) + .orElseThrow(() -> new RuntimeException("Submission not linked to any assignment: " + submissionId)); + } + + @Override + @Transactional + public void updateStatus(UUID id, AudioProcessingStatus status, String errorMessage) { + repository.updateStatus(id, status, errorMessage); + } +} diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/AssignmentEntity.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/AssignmentEntity.java new file mode 100644 index 0000000..fe7959a --- /dev/null +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/AssignmentEntity.java @@ -0,0 +1,29 @@ +package com.smartjam.smartjamanalyzer.infrastructure.persistence.entity; + +import java.util.UUID; + +import jakarta.persistence.*; + +import com.smartjam.common.model.AudioProcessingStatus; +import lombok.*; + +@Entity +@Table(name = "assignments") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AssignmentEntity { + @Id + private UUID id; + + @Enumerated(EnumType.STRING) + private AudioProcessingStatus status; + + @Column(name = "reference_spectre_cache") + private byte[] referenceSpectreCache; + + @Column(name = "error_message") + private String errorMessage; +} diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/SubmissionEntity.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/SubmissionEntity.java new file mode 100644 index 0000000..06e2962 --- /dev/null +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/SubmissionEntity.java @@ -0,0 +1,47 @@ +package com.smartjam.smartjamanalyzer.infrastructure.persistence.entity; + +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.*; + +import com.smartjam.common.dto.FeedbackEvent; +import com.smartjam.common.model.AudioProcessingStatus; +import lombok.*; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Entity +@Table(name = "submissions") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubmissionEntity { + + @Id + private UUID id; + + @Column(name = "assignment_id") + private UUID assignmentId; + + @Enumerated(EnumType.STRING) + private AudioProcessingStatus status; + + @Column(name = "total_score") + private Double totalScore; + + @Column(name = "pitch_score") + private Double pitchScore; + + @Column(name = "rhythm_score") + private Double rhythmScore; + + @Column(name = "error_message") + private String errorMessage; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "analysis_feedback") + private List analysisFeedback; +} diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaAssignmentRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaAssignmentRepository.java new file mode 100644 index 0000000..d06f3cf --- /dev/null +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaAssignmentRepository.java @@ -0,0 +1,16 @@ +package com.smartjam.smartjamanalyzer.infrastructure.persistence.repository; + +import java.util.UUID; + +import com.smartjam.common.model.AudioProcessingStatus; +import com.smartjam.smartjamanalyzer.infrastructure.persistence.entity.AssignmentEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +public interface JpaAssignmentRepository extends JpaRepository { + + @Modifying(clearAutomatically = true) + @Query("UPDATE AssignmentEntity a SET a.status = :status, a.errorMessage = :error WHERE a.id " + "= :id") + void updateStatus(UUID id, AudioProcessingStatus status, String error); +} diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaSubmissionRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaSubmissionRepository.java new file mode 100644 index 0000000..1796b33 --- /dev/null +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaSubmissionRepository.java @@ -0,0 +1,16 @@ +package com.smartjam.smartjamanalyzer.infrastructure.persistence.repository; + +import java.util.UUID; + +import com.smartjam.common.model.AudioProcessingStatus; +import com.smartjam.smartjamanalyzer.infrastructure.persistence.entity.SubmissionEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +public interface JpaSubmissionRepository extends JpaRepository { + + @Modifying(clearAutomatically = true) + @Query("UPDATE SubmissionEntity s SET s.status = :status, s.errorMessage = :error WHERE s.id " + "= :id") + void updateStatus(UUID id, AudioProcessingStatus status, String error); +} diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java index e32c756..155b66b 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java @@ -6,10 +6,15 @@ import java.util.List; import java.util.Objects; -/** Binary serializer for spectral feature matrices. Storage format: [int:N frames][int:M bins][N * M raw floats] */ +import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; + +/** + * Binary serializer for spectral feature matrices. Storage format: [int:N frames][int:M bins][float: framerate][N * M + * raw floats] + */ public class FeatureBinarySerializer { - private static final int HEADER_SIZE = 8; + private static final int HEADER_SIZE = 12; private FeatureBinarySerializer() { // utility class, no need to be constructed @@ -18,20 +23,22 @@ private FeatureBinarySerializer() { /** * Serializes a list of equally sized float frames into a byte array. * - * @param frames list of frames (each frame is an array of floats) + * @param sequence list of frames (each frame is an array of floats) * @return byte array with little-endian representation * @throws IllegalArgumentException if frames are null, empty, or frames have different lengths */ - public static byte[] serialize(List frames) { + public static byte[] serialize(FeatureSequence sequence) { - Objects.requireNonNull(frames, "frames list must not be null"); + Objects.requireNonNull(sequence, "Feature sequence must not be null"); - if (frames.isEmpty()) { + if (sequence.frames().isEmpty()) { return new byte[0]; } + List frames = sequence.frames(); int frameCount = frames.size(); int binCount = frames.getFirst().length; + float frameRate = sequence.frameRate(); for (var frame : frames) { if (frame.length != binCount) { @@ -51,6 +58,7 @@ public static byte[] serialize(List frames) { buffer.putInt(frameCount); buffer.putInt(binCount); + buffer.putFloat(frameRate); for (float[] frame : frames) { for (float val : frame) { @@ -64,19 +72,20 @@ public static byte[] serialize(List frames) { /** * Deserializes a byte array back to a list of frames. * - * @param data byte array produced by {@link #serialize(List)} - * @return list of frames, or empty list if data is null or too short + * @param data byte array produced by {@link #serialize(FeatureSequence)} + * @return FeatureSequence, or null if data is null or too short * @throws IllegalArgumentException if the data is malformed (e.g., negative sizes, insufficient length) */ - public static List deserialize(byte[] data) { + public static FeatureSequence deserialize(byte[] data) { if (data == null || data.length < HEADER_SIZE) { - return new ArrayList<>(); + return null; } ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); int frameCount = buffer.getInt(); int binCount = buffer.getInt(); + float frameRate = buffer.getFloat(); if (frameCount < 0 || binCount < 0) { throw new IllegalArgumentException("Invalid header: frameCount or binCount is negative"); @@ -102,6 +111,6 @@ public static List deserialize(byte[] data) { } frames.add(frame); } - return frames; + return new FeatureSequence(frames, frameRate); } } diff --git a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCaseTest.java b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCaseTest.java index 10ad953..ec50419 100644 --- a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCaseTest.java +++ b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCaseTest.java @@ -2,7 +2,9 @@ import java.nio.file.Path; import java.util.List; +import java.util.UUID; +import com.smartjam.common.model.AudioProcessingStatus; import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; import com.smartjam.smartjamanalyzer.domain.port.*; import org.junit.jupiter.api.DisplayName; @@ -16,11 +18,13 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class AudioAnalysisUseCaseTest { + private final String VALID_UUID_STR = "11111111-1111-1111-1111-111111111111"; + private final UUID VALID_UUID = UUID.fromString(VALID_UUID_STR); + @Mock private AudioStorage storage; @@ -36,39 +40,58 @@ class AudioAnalysisUseCaseTest { @Mock private FeatureExtractor featureExtractor; + @Mock + private PerformanceEvaluator performanceEvaluator; + + @Mock + private ReferenceRepository referenceRepository; + + @Mock + private ResultRepository resultRepository; + + @Mock + private DebugVisualizer debugVisualizer; + @InjectMocks private AudioAnalysisUseCase useCase; @Test @DisplayName("UseCase должен сначала скачать, потом конвертировать") void shouldProcessInOrder() { - String bucket = "sub"; - String key = "test.mp3"; + String bucket = "references"; + String fileKey = VALID_UUID_STR + ".m4a"; Path mockPath = Path.of("input"); - Path mockClean = Path.of("output"); - FeatureSequence mockSequence = new FeatureSequence(List.of(new float[84]), 20f); + Path mockWav = Path.of("output"); + FeatureSequence mockSeq = new FeatureSequence(List.of(new float[84]), 20f); - when(storage.downloadAudioFile(eq(bucket), eq(key), any())).thenReturn(mockPath); - when(converter.convertToStandardWav(eq(mockPath), any())).thenReturn(mockClean); when(workspaceFactory.create()).thenReturn(workspace); - when(featureExtractor.extract(eq(mockClean))).thenReturn(mockSequence); + when(storage.downloadAudioFile(eq(bucket), eq(fileKey), any())).thenReturn(mockPath); + when(converter.convertToStandardWav(eq(mockPath), any())).thenReturn(mockWav); + when(featureExtractor.extract(mockWav)).thenReturn(mockSeq); - useCase.execute(bucket, key); + useCase.execute(bucket, fileKey); - InOrder inOrder = inOrder(storage, converter, featureExtractor); - inOrder.verify(storage).downloadAudioFile(eq(bucket), eq(key), any()); + InOrder inOrder = inOrder(referenceRepository, storage, converter, featureExtractor); + + inOrder.verify(referenceRepository).updateStatus(VALID_UUID, AudioProcessingStatus.ANALYZING, null); + + inOrder.verify(storage).downloadAudioFile(eq(bucket), eq(fileKey), any()); inOrder.verify(converter).convertToStandardWav(eq(mockPath), any()); - inOrder.verify(featureExtractor).extract(mockClean); + + inOrder.verify(featureExtractor).extract(mockWav); + inOrder.verify(referenceRepository).save(VALID_UUID, mockSeq); } @Test - @DisplayName("UseCase должен бросать ошибку, если конвертация зависла") + @DisplayName("UseCase должен бросать ошибку, если конвертация зависла и писать FAILED в БД") void shouldThrowExceptionWhenConverterTimesOut() { when(workspaceFactory.create()).thenReturn(workspace); when(storage.downloadAudioFile(any(), any(), any())).thenReturn(Path.of("input")); when(converter.convertToStandardWav(any(), any())).thenThrow(new RuntimeException("FFmpeg timeout exceeded")); - assertThrows(RuntimeException.class, () -> useCase.execute("sub", "key.mp3")); + assertThrows(RuntimeException.class, () -> useCase.execute("submissions", VALID_UUID_STR)); + + verify(resultRepository).updateStatus(VALID_UUID, AudioProcessingStatus.FAILED, "FFmpeg timeout exceeded"); } @Test @@ -79,9 +102,18 @@ void shouldWrapStorageException() { when(workspaceFactory.create()).thenReturn(workspace); when(storage.downloadAudioFile(any(), any(), any())).thenThrow(new RuntimeException(errorMessage)); - RuntimeException exception = assertThrows(RuntimeException.class, () -> useCase.execute("sub", "key.mp3")); + RuntimeException exception = + assertThrows(RuntimeException.class, () -> useCase.execute("references", VALID_UUID_STR)); assertTrue(exception.getMessage().contains("Business logic failed")); assertEquals(errorMessage, exception.getCause().getMessage()); + + verify(referenceRepository).updateStatus(VALID_UUID, AudioProcessingStatus.FAILED, errorMessage); + } + + @Test + @DisplayName("UseCase должен выкинуть исключение, если ключ не UUID") + void shouldThrowOnInvalidUuidKey() { + assertThrows(IllegalArgumentException.class, () -> useCase.execute("submissions", "not-a-uuid.mp3")); } } diff --git a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java index 208bc86..9e408a5 100644 --- a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java +++ b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; +import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,42 +13,32 @@ class FeatureBinarySerializerTest { + private static final float TEST_FRAME_RATE = 21.53f; + @Test @DisplayName("Сериализация и десериализация сохраняет данные") void shouldMaintainDataIntegrity() { float[] frame1 = {0.1f, 0.5f, 0.9f}; float[] frame2 = {0.2f, 0.4f, 0.6f}; - List original = List.of(frame1, frame2); + FeatureSequence original = new FeatureSequence(List.of(frame1, frame2), TEST_FRAME_RATE); byte[] encoded = FeatureBinarySerializer.serialize(original); assertNotNull(encoded); assertTrue(encoded.length > 8); + assertEquals(36, encoded.length); - List decoded = FeatureBinarySerializer.deserialize(encoded); + FeatureSequence decoded = FeatureBinarySerializer.deserialize(encoded); - assertEquals(original.size(), decoded.size()); - assertEquals(original.getFirst().length, decoded.getFirst().length); + assertEquals(original.frameRate(), decoded.frameRate(), 1e-6f); + assertEquals(original.frames().size(), decoded.frames().size()); + assertEquals(original.frames().getFirst().length, decoded.frames().getFirst().length); - for (int i = 0; i < original.size(); i++) { - assertArrayEquals(original.get(i), decoded.get(i), 1e-6f); + for (int i = 0; i < original.frames().size(); i++) { + assertArrayEquals(original.frames().get(i), decoded.frames().get(i), 1e-6f); } } - @Test - @DisplayName("Пустой список возвращает пустой массив и пустой результат") - void emptyList() { - - List empty = List.of(); - byte[] encoded = FeatureBinarySerializer.serialize(empty); - - assertEquals(0, encoded.length); - - List decoded = FeatureBinarySerializer.deserialize(encoded); - - assertTrue(decoded.isEmpty()); - } - @Test @DisplayName("Сериализация с null вызывает NullPointerException") void serializeNullThrows() { @@ -55,36 +46,29 @@ void serializeNullThrows() { assertThrows(NullPointerException.class, () -> FeatureBinarySerializer.serialize(null)); } - @Test - @DisplayName("Десериализация null возвращает пустой список") - void deserializeNullReturnsEmpty() { - - List result = FeatureBinarySerializer.deserialize(null); - assertTrue(result.isEmpty()); - } - @Test @DisplayName("Десериализация слишком короткого массива возвращает пустой список") void deserializeTooShortReturnsEmpty() { byte[] shortData = new byte[4]; - List result = FeatureBinarySerializer.deserialize(shortData); - assertTrue(result.isEmpty()); + assertNull(FeatureBinarySerializer.deserialize(shortData)); } @Test @DisplayName("Сериализация с фреймами разной длины вызывает исключение") void serializeFramesWithDifferentLengthsThrows() { - - List frames = List.of(new float[] {1.0f, 2.0f}, new float[] {3.0f, 4.0f, 5.0f}); - assertThrows(IllegalArgumentException.class, () -> FeatureBinarySerializer.serialize(frames)); + // Технически, исключение вылетает вообще из конструктора FeatureSequence + assertThrows( + IllegalArgumentException.class, + () -> FeatureBinarySerializer.serialize(new FeatureSequence( + List.of(new float[] {1.0f, 2.0f}, new float[] {3.0f, 4.0f, 5.0f}), TEST_FRAME_RATE))); } @Test @DisplayName("Десериализация с отрицательным frameCount вызывает исключение") void deserializeNegativeFrameCountThrows() { - ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer buffer = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN); buffer.putInt(-1); buffer.putInt(10); byte[] data = buffer.array(); @@ -96,7 +80,7 @@ void deserializeNegativeFrameCountThrows() { @DisplayName("Десериализация с отрицательным binCount вызывает исключение") void deserializeNegativeBinCountThrows() { - ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer buffer = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN); buffer.putInt(5); buffer.putInt(-3); byte[] data = buffer.array(); @@ -123,12 +107,14 @@ void deserializeInsufficientDataThrows() { void littleEndianOrder() { List frames = List.of(new float[] {1.0f}); - byte[] encoded = FeatureBinarySerializer.serialize(frames); + FeatureSequence seq = new FeatureSequence(frames, TEST_FRAME_RATE); + byte[] encoded = FeatureBinarySerializer.serialize(seq); ByteBuffer buffer = ByteBuffer.wrap(encoded).order(ByteOrder.LITTLE_ENDIAN); - assertEquals(1, buffer.getInt()); - assertEquals(1, buffer.getInt()); - assertEquals(1.0f, buffer.getFloat(), 1e-6f); + assertEquals(1, buffer.getInt(), "Frame count mismatch"); + assertEquals(1, buffer.getInt(), "Bin count mismatch"); + assertEquals(TEST_FRAME_RATE, buffer.getFloat(), 1e-6f, "Frame rate mismatch"); + assertEquals(1.0f, buffer.getFloat(), 1e-6f, "First data value mismatch"); } @Test @@ -148,13 +134,17 @@ void largeData() { frames.add(frame); } - byte[] encoded = FeatureBinarySerializer.serialize(frames); - List decoded = FeatureBinarySerializer.deserialize(encoded); + FeatureSequence original = new FeatureSequence(frames, TEST_FRAME_RATE); + + byte[] encoded = FeatureBinarySerializer.serialize(original); - assertEquals(framesCount, decoded.size()); + FeatureSequence decoded = FeatureBinarySerializer.deserialize(encoded); + + assertEquals(framesCount, decoded.frames().size()); + assertEquals(TEST_FRAME_RATE, decoded.frameRate()); for (int i = 0; i < framesCount; i++) { - assertArrayEquals(frames.get(i), decoded.get(i), 1e-6f); + assertArrayEquals(frames.get(i), decoded.frames().get(i), 1e-6f); } } @@ -163,13 +153,14 @@ void largeData() { void singleFrameSingleValue() { List original = List.of(new float[] {42.0f}); + FeatureSequence seq = new FeatureSequence(original, TEST_FRAME_RATE); - byte[] encoded = FeatureBinarySerializer.serialize(original); - List decoded = FeatureBinarySerializer.deserialize(encoded); + byte[] encoded = FeatureBinarySerializer.serialize(seq); + FeatureSequence decoded = FeatureBinarySerializer.deserialize(encoded); - assertEquals(1, decoded.size()); - assertEquals(1, decoded.getFirst().length); - assertEquals(42.0f, decoded.getFirst()[0], 1e-6f); + assertEquals(1, decoded.frames().size()); + assertEquals(1, decoded.frames().getFirst().length); + assertEquals(42.0f, decoded.frames().getFirst()[0], 1e-6f); } @Test @@ -179,11 +170,14 @@ void extremeFloatValues() { List frames = List.of( new float[] {Float.MAX_VALUE, -Float.MAX_VALUE, Float.MIN_VALUE, Float.NaN, Float.POSITIVE_INFINITY}); - byte[] encoded = FeatureBinarySerializer.serialize(frames); - List decoded = FeatureBinarySerializer.deserialize(encoded); + FeatureSequence seq = new FeatureSequence(frames, TEST_FRAME_RATE); + + byte[] encoded = FeatureBinarySerializer.serialize(seq); + + FeatureSequence decoded = FeatureBinarySerializer.deserialize(encoded); float[] originalFrame = frames.getFirst(); - float[] decodedFrame = decoded.getFirst(); + float[] decodedFrame = decoded.frames().getFirst(); assertEquals(originalFrame.length, decodedFrame.length); for (int i = 0; i < originalFrame.length; i++) { if (Float.isNaN(originalFrame[i])) { @@ -198,7 +192,7 @@ void extremeFloatValues() { @DisplayName("Защита от переполнения при десериализации") void deserializeOverflowThrows() { - ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer buffer = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN); int large = Integer.MAX_VALUE / 1000; buffer.putInt(large); From a55720ab5e4eca878da6c215f1e0c92468ffb156 Mon Sep 17 00:00:00 2001 From: Satlykovs Date: Sun, 22 Mar 2026 13:46:42 +0300 Subject: [PATCH 3/4] refactor: address review feedback and add javadocs --- .../application/AudioAnalysisUseCase.java | 22 +++++++++++----- .../domain/port/ReferenceRepository.java | 12 +++++++-- .../domain/port/ResultRepository.java | 26 ++++++++++++++++++- .../adapter/AssignmentPersistenceAdapter.java | 23 ++++++++++++---- .../adapter/SubmissionPersistenceAdapter.java | 12 +++++++-- .../persistence/entity/AssignmentEntity.java | 3 ++- .../persistence/entity/SubmissionEntity.java | 4 +++ .../repository/JpaAssignmentRepository.java | 4 +++ .../repository/JpaSubmissionRepository.java | 11 ++++++++ .../utils/FeatureBinarySerializer.java | 9 ++++--- .../utils/FeatureBinarySerializerTest.java | 4 +-- 11 files changed, 106 insertions(+), 24 deletions(-) diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java index c6f47d1..af7b36c 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java @@ -66,8 +66,13 @@ public void execute(String bucket, String fileKey) { log.info("Результаты обработки {}: \n{}", fileKey, watch.prettyPrint()); } catch (Exception e) { - log.error("Ошибка в UseCase для файла {}: {}", fileKey, e.getMessage(), e); + + String errorMsg = + e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(); + + log.error("Ошибка в UseCase для файла {}: {}", fileKey, errorMsg, e); updateStatus(bucket, entityId, AudioProcessingStatus.FAILED, e.getMessage()); + throw new RuntimeException("Business logic failed", e); } } @@ -94,17 +99,20 @@ private void handleStudentSubmission(UUID submissionId, FeatureSequence studentF UUID assignmentId = resultRepository.findAssignmentIdBySubmissionId(submissionId); - FeatureSequence teacherFeatures = referenceRepository.findById(assignmentId); - - if (teacherFeatures == null) { - throw new IllegalStateException("Reference features not ready for assignment: " + assignmentId); - } + FeatureSequence teacherFeatures = referenceRepository + .findFeaturesById(assignmentId) + .orElseThrow((() -> new IllegalArgumentException( + "Teacher reference features not found for assignment: " + assignmentId))); AnalysisResult result = performanceEvaluator.evaluate(teacherFeatures, studentFeatures); resultRepository.save(submissionId, result); - debugVisualizer.generateHeatmap(result, "debug_" + submissionId + ".png"); + try { + debugVisualizer.generateHeatmap(result, "debug_" + submissionId + ".png"); + } catch (Exception e) { + log.error("Не удалось сгенерировать тепловую карту {}: {}", submissionId, e.getMessage()); + } log.info("Submission {} evaluation completed.", submissionId); } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java index 05a3862..c422582 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java @@ -1,5 +1,6 @@ package com.smartjam.smartjamanalyzer.domain.port; +import java.util.Optional; import java.util.UUID; import com.smartjam.common.model.AudioProcessingStatus; @@ -22,7 +23,14 @@ public interface ReferenceRepository { * @param assignmentId Unique identifier of the assignment. * @return The feature sequence or null if not found. */ - FeatureSequence findById(UUID assignmentId); + Optional findFeaturesById(UUID assignmentId); - void updateStatus(UUID id, AudioProcessingStatus status, String errorMessage); + /** + * Performs an optimized status update for an assignment, optionally recording an error message. + * + * @param assignmentId unique identifier of the submission. + * @param status the new processing state. + * @param errorMessage description of the failure, if applicable. + */ + void updateStatus(UUID assignmentId, AudioProcessingStatus status, String errorMessage); } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java index b252c84..a097bba 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java @@ -5,11 +5,35 @@ import com.smartjam.common.model.AudioProcessingStatus; import com.smartjam.smartjamanalyzer.domain.model.AnalysisResult; +/** + * Port for managing student submissions and their analysis results. Handles the persistence of evaluation scores and + * detailed feedback events. + */ public interface ResultRepository { + /** + * Persists the final analysis result for a specific submission. + * + * @param submissionId unique identifier of the student submission. + * @param result calculated scores and feedback events. + */ void save(UUID submissionId, AnalysisResult result); + /** + * Resolves the teacher's assignment ID associated with a given student submission. + * + * @param submissionId unique identifier of the submission. + * @return the UUID of the parent assignment. + * @throws RuntimeException if no linked assignment ID found. + */ UUID findAssignmentIdBySubmissionId(UUID submissionId); - void updateStatus(UUID id, AudioProcessingStatus status, String errorMessage); + /** + * Update status for a submission, optionally recording an error message. + * + * @param submissionId unique identifier of the submission. + * @param status the new processing state. + * @param errorMessage description of the failure, if applicable. + */ + void updateStatus(UUID submissionId, AudioProcessingStatus status, String errorMessage); } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/AssignmentPersistenceAdapter.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/AssignmentPersistenceAdapter.java index fc6694f..fe354a2 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/AssignmentPersistenceAdapter.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/AssignmentPersistenceAdapter.java @@ -1,9 +1,8 @@ package com.smartjam.smartjamanalyzer.infrastructure.persistence.adapter; +import java.util.Optional; import java.util.UUID; -import jakarta.transaction.Transactional; - import com.smartjam.common.model.AudioProcessingStatus; import com.smartjam.smartjamanalyzer.domain.model.FeatureSequence; import com.smartjam.smartjamanalyzer.domain.port.ReferenceRepository; @@ -12,12 +11,21 @@ import com.smartjam.smartjamanalyzer.infrastructure.utils.FeatureBinarySerializer; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +/** + * JPA implementation of {@link ReferenceRepository}. Bridges the domain logic with the database using binary + * serialization for spectral data. + */ @Component @RequiredArgsConstructor public class AssignmentPersistenceAdapter implements ReferenceRepository { private final JpaAssignmentRepository repository; + /** + * Packs spectral features into a binary format and saves them to the assignment record. Transition the status to + * {@link AudioProcessingStatus#COMPLETED}. + */ @Override @Transactional public void save(UUID assignmentId, FeatureSequence features) { @@ -35,12 +43,17 @@ public void save(UUID assignmentId, FeatureSequence features) { repository.save(entity); } + /** + * Retrieves and unpacks binary features from the database. + * + * @return an Optional containing the FeatureSequence, or empty if not found. + */ @Override - public FeatureSequence findById(UUID assignmentId) { + @Transactional(readOnly = true) + public Optional findFeaturesById(UUID assignmentId) { return repository .findById(assignmentId) - .map(e -> FeatureBinarySerializer.deserialize(e.getReferenceSpectreCache())) - .orElse(null); + .map(e -> FeatureBinarySerializer.deserialize(e.getReferenceSpectreCache())); } @Override diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java index 3070d8b..ff7d4f4 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java @@ -2,8 +2,6 @@ import java.util.UUID; -import jakarta.transaction.Transactional; - import com.smartjam.common.model.AudioProcessingStatus; import com.smartjam.smartjamanalyzer.domain.model.AnalysisResult; import com.smartjam.smartjamanalyzer.domain.port.ResultRepository; @@ -11,12 +9,21 @@ import com.smartjam.smartjamanalyzer.infrastructure.persistence.repository.JpaSubmissionRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +/** + * JPA implementation of {@link ResultRepository}. Manages storage of evaluation results and coordinates mapping between + * domain results and JSONB database columns. + */ @Component @RequiredArgsConstructor public class SubmissionPersistenceAdapter implements ResultRepository { private final JpaSubmissionRepository repository; + /** + * Updates the submission record with scores and feedback. Transition the status to + * {@link AudioProcessingStatus#COMPLETED}. + */ @Override @Transactional public void save(UUID submissionId, AnalysisResult result) { @@ -35,6 +42,7 @@ public void save(UUID submissionId, AnalysisResult result) { } @Override + @Transactional(readOnly = true) public UUID findAssignmentIdBySubmissionId(UUID submissionId) { return repository .findById(submissionId) diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/AssignmentEntity.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/AssignmentEntity.java index fe7959a..0c74d06 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/AssignmentEntity.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/AssignmentEntity.java @@ -7,6 +7,7 @@ import com.smartjam.common.model.AudioProcessingStatus; import lombok.*; +/** Database model for teacher assignments. Stores heavy spectral data as raw bytes (BYTEA) to optimize performance. */ @Entity @Table(name = "assignments") @Getter @@ -24,6 +25,6 @@ public class AssignmentEntity { @Column(name = "reference_spectre_cache") private byte[] referenceSpectreCache; - @Column(name = "error_message") + @Column(name = "error_message", columnDefinition = "TEXT") private String errorMessage; } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/SubmissionEntity.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/SubmissionEntity.java index 06e2962..0340165 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/SubmissionEntity.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/entity/SubmissionEntity.java @@ -11,6 +11,10 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; +/** + * Database model for student submissions. Utilizes Hibernate 6 JSON support to store structured feedback events in a + * JSONB column. + */ @Entity @Table(name = "submissions") @Getter diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaAssignmentRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaAssignmentRepository.java index d06f3cf..3b5185e 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaAssignmentRepository.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaAssignmentRepository.java @@ -8,8 +8,12 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +/** Spring Data JPA repository for {@link AssignmentEntity}. Includes query for status management. */ public interface JpaAssignmentRepository extends JpaRepository { + /** + * Performs an update on the record status and error message without loading large binary spectral data into memory. + */ @Modifying(clearAutomatically = true) @Query("UPDATE AssignmentEntity a SET a.status = :status, a.errorMessage = :error WHERE a.id " + "= :id") void updateStatus(UUID id, AudioProcessingStatus status, String error); diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaSubmissionRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaSubmissionRepository.java index 1796b33..792d99f 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaSubmissionRepository.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/repository/JpaSubmissionRepository.java @@ -8,8 +8,19 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +/** + * Spring Data JPA repository for {@link SubmissionEntity}. Manages persistence for student attempts and their + * corresponding analysis metrics. + */ public interface JpaSubmissionRepository extends JpaRepository { + /** + * Updates the status and error message of a submission using a partial update query. * + * + * @param id Unique identifier of the submission. + * @param status The new processing status to be set. + * @param error Error message if the status is FAILED, otherwise null. + */ @Modifying(clearAutomatically = true) @Query("UPDATE SubmissionEntity s SET s.status = :status, s.errorMessage = :error WHERE s.id " + "= :id") void updateStatus(UUID id, AudioProcessingStatus status, String error); diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java index 155b66b..7b748d4 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java @@ -25,6 +25,7 @@ private FeatureBinarySerializer() { * * @param sequence list of frames (each frame is an array of floats) * @return byte array with little-endian representation + * @throws NullPointerException if sequence is null. * @throws IllegalArgumentException if frames are null, empty, or frames have different lengths */ public static byte[] serialize(FeatureSequence sequence) { @@ -47,12 +48,12 @@ public static byte[] serialize(FeatureSequence sequence) { } } - long totalElements = (long) frameCount * binCount; - if (totalElements > Integer.MAX_VALUE / Float.BYTES) { - throw new IllegalArgumentException("Too many elements to serialize: " + totalElements); + long totalPayloadBytes = (long) frameCount * binCount * Float.BYTES; + if (totalPayloadBytes > Integer.MAX_VALUE - HEADER_SIZE) { + throw new IllegalArgumentException("Matrix is too large for binary serialization"); } - int totalBytes = HEADER_SIZE + frameCount * binCount * Float.BYTES; + int totalBytes = HEADER_SIZE + (int) totalPayloadBytes; ByteBuffer buffer = ByteBuffer.allocate(totalBytes).order(ByteOrder.LITTLE_ENDIAN); diff --git a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java index 9e408a5..b12df71 100644 --- a/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java +++ b/backend/smartjam-analyzer/src/test/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializerTest.java @@ -47,8 +47,8 @@ void serializeNullThrows() { } @Test - @DisplayName("Десериализация слишком короткого массива возвращает пустой список") - void deserializeTooShortReturnsEmpty() { + @DisplayName("Десериализация слишком короткого массива возвращает null") + void deserializeTooShortReturnsNull() { byte[] shortData = new byte[4]; assertNull(FeatureBinarySerializer.deserialize(shortData)); From 814ee9e1db81919556214b87daa2533d1c820d2c Mon Sep 17 00:00:00 2001 From: Satlykovs Date: Sun, 22 Mar 2026 14:03:17 +0300 Subject: [PATCH 4/4] refactor: address review feedback --- .../application/AudioAnalysisUseCase.java | 11 +++++++---- .../domain/port/ReferenceRepository.java | 2 +- .../domain/port/ResultRepository.java | 3 ++- .../adapter/SubmissionPersistenceAdapter.java | 12 +++++------- .../utils/FeatureBinarySerializer.java | 11 +++++------ 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java index af7b36c..61f3ad4 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/application/AudioAnalysisUseCase.java @@ -71,7 +71,7 @@ public void execute(String bucket, String fileKey) { e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(); log.error("Ошибка в UseCase для файла {}: {}", fileKey, errorMsg, e); - updateStatus(bucket, entityId, AudioProcessingStatus.FAILED, e.getMessage()); + updateStatus(bucket, entityId, AudioProcessingStatus.FAILED, errorMsg); throw new RuntimeException("Business logic failed", e); } @@ -97,12 +97,15 @@ private void handleTeacherReference(UUID assignmentId, FeatureSequence teacherFe private void handleStudentSubmission(UUID submissionId, FeatureSequence studentFeatures) { log.info("Evaluating student submission: {}", submissionId); - UUID assignmentId = resultRepository.findAssignmentIdBySubmissionId(submissionId); + UUID assignmentId = resultRepository + .findAssignmentIdBySubmissionId(submissionId) + .orElseThrow(() -> + new IllegalStateException("Submission " + submissionId + " is not linked to any assignment")); FeatureSequence teacherFeatures = referenceRepository .findFeaturesById(assignmentId) - .orElseThrow((() -> new IllegalArgumentException( - "Teacher reference features not found for assignment: " + assignmentId))); + .orElseThrow(() -> new IllegalStateException( + "Teacher reference features not found for assignment: " + assignmentId)); AnalysisResult result = performanceEvaluator.evaluate(teacherFeatures, studentFeatures); diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java index c422582..5826eaa 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ReferenceRepository.java @@ -21,7 +21,7 @@ public interface ReferenceRepository { * Retrieves reference features for comparison. * * @param assignmentId Unique identifier of the assignment. - * @return The feature sequence or null if not found. + * @return The feature sequence if presented. */ Optional findFeaturesById(UUID assignmentId); diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java index a097bba..3a37094 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/domain/port/ResultRepository.java @@ -1,5 +1,6 @@ package com.smartjam.smartjamanalyzer.domain.port; +import java.util.Optional; import java.util.UUID; import com.smartjam.common.model.AudioProcessingStatus; @@ -26,7 +27,7 @@ public interface ResultRepository { * @return the UUID of the parent assignment. * @throws RuntimeException if no linked assignment ID found. */ - UUID findAssignmentIdBySubmissionId(UUID submissionId); + Optional findAssignmentIdBySubmissionId(UUID submissionId); /** * Update status for a submission, optionally recording an error message. diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java index ff7d4f4..c1b73dc 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/persistence/adapter/SubmissionPersistenceAdapter.java @@ -1,5 +1,6 @@ package com.smartjam.smartjamanalyzer.infrastructure.persistence.adapter; +import java.util.Optional; import java.util.UUID; import com.smartjam.common.model.AudioProcessingStatus; @@ -43,16 +44,13 @@ public void save(UUID submissionId, AnalysisResult result) { @Override @Transactional(readOnly = true) - public UUID findAssignmentIdBySubmissionId(UUID submissionId) { - return repository - .findById(submissionId) - .map(SubmissionEntity::getAssignmentId) - .orElseThrow(() -> new RuntimeException("Submission not linked to any assignment: " + submissionId)); + public Optional findAssignmentIdBySubmissionId(UUID submissionId) { + return repository.findById(submissionId).map(SubmissionEntity::getAssignmentId); } @Override @Transactional - public void updateStatus(UUID id, AudioProcessingStatus status, String errorMessage) { - repository.updateStatus(id, status, errorMessage); + public void updateStatus(UUID submissionId, AudioProcessingStatus status, String errorMessage) { + repository.updateStatus(submissionId, status, errorMessage); } } diff --git a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java index 7b748d4..a83b16f 100644 --- a/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java +++ b/backend/smartjam-analyzer/src/main/java/com/smartjam/smartjamanalyzer/infrastructure/utils/FeatureBinarySerializer.java @@ -26,7 +26,7 @@ private FeatureBinarySerializer() { * @param sequence list of frames (each frame is an array of floats) * @return byte array with little-endian representation * @throws NullPointerException if sequence is null. - * @throws IllegalArgumentException if frames are null, empty, or frames have different lengths + * @throws IllegalArgumentException if frames are null or have different lengths */ public static byte[] serialize(FeatureSequence sequence) { @@ -92,13 +92,12 @@ public static FeatureSequence deserialize(byte[] data) { throw new IllegalArgumentException("Invalid header: frameCount or binCount is negative"); } - long totalElements = (long) frameCount * binCount; - if (totalElements > Integer.MAX_VALUE / Float.BYTES) { - throw new IllegalArgumentException("Too many elements to deserialize: " + totalElements); + long totalPayloadBytes = (long) frameCount * binCount * Float.BYTES; + if (totalPayloadBytes > Integer.MAX_VALUE - HEADER_SIZE) { + throw new IllegalArgumentException("Too many elements to deserialize: " + totalPayloadBytes); } - int expectedBytes = HEADER_SIZE + frameCount * binCount * Float.BYTES; - + int expectedBytes = HEADER_SIZE + (int) totalPayloadBytes; if (data.length < expectedBytes) { throw new IllegalArgumentException( "Data too short. Expected " + expectedBytes + " bytes, got " + data.length);