This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package ru.soune.nocopy.configuration;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(30000);
|
||||
requestFactory.setReadTimeout(60000);
|
||||
restTemplate.setRequestFactory(requestFactory);
|
||||
|
||||
return restTemplate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class GoogleVisionSearchResponse {
|
||||
|
||||
@JsonProperty("bestGuessLabels")
|
||||
private List<BestGuessLabel> bestGuessLabels;
|
||||
|
||||
@JsonProperty("fullMatchingImages")
|
||||
private List<ImageResult> fullMatchingImages;
|
||||
|
||||
@JsonProperty("visuallySimilarImages")
|
||||
private List<ImageResult> visuallySimilarImages;
|
||||
|
||||
@JsonProperty("pagesWithMatchingImages")
|
||||
private List<PageResult> pagesWithMatchingImages;
|
||||
|
||||
@JsonProperty("partialMatchingImages")
|
||||
private List<ImageResult> partialMatchingImages;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class BestGuessLabel {
|
||||
@JsonProperty("label")
|
||||
private String label;
|
||||
|
||||
@JsonProperty("languageCode")
|
||||
private String languageCode;
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ImageResult {
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
|
||||
@JsonProperty("score")
|
||||
private Float score;
|
||||
|
||||
@JsonProperty("height")
|
||||
private Integer height;
|
||||
|
||||
@JsonProperty("width")
|
||||
private Integer width;
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class PageResult {
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
|
||||
@JsonProperty("pageTitle")
|
||||
private String pageTitle;
|
||||
|
||||
@JsonProperty("fullMatchingImages")
|
||||
private List<ImageResult> fullMatchingImages;
|
||||
|
||||
@JsonProperty("partialMatchingImages")
|
||||
private List<ImageResult> partialMatchingImages;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@ package ru.soune.nocopy.handler;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.soune.nocopy.dto.BaseRequest;
|
||||
import ru.soune.nocopy.dto.BaseResponse;
|
||||
import ru.soune.nocopy.dto.MessageCode;
|
||||
import ru.soune.nocopy.dto.file.GoogleVisionSearchResponse;
|
||||
import ru.soune.nocopy.dto.file.ImageSearchRequest;
|
||||
import ru.soune.nocopy.dto.file.YandexSearchResponse;
|
||||
import ru.soune.nocopy.service.GoogleVisionSearchService;
|
||||
import ru.soune.nocopy.service.YandexSearchService;
|
||||
|
||||
@Slf4j
|
||||
@@ -21,6 +22,8 @@ public class ImageFoundRequestHandler implements RequestHandler {
|
||||
|
||||
private final YandexSearchService yandexSearchService;
|
||||
|
||||
private final GoogleVisionSearchService googleVisionSearchService;
|
||||
|
||||
@Override
|
||||
public BaseResponse handle(BaseRequest request) throws Exception {
|
||||
ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(),
|
||||
@@ -30,6 +33,9 @@ public class ImageFoundRequestHandler implements RequestHandler {
|
||||
|
||||
YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileId);
|
||||
|
||||
//TODO uncommited when add billing
|
||||
// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId);
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
||||
MessageCode.SUCCESS.getDescription(), response);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package ru.soune.nocopy.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
import com.google.cloud.vision.v1.*;
|
||||
import com.google.protobuf.ByteString;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.soune.nocopy.dto.BaseResponse;
|
||||
import ru.soune.nocopy.dto.MessageCode;
|
||||
import ru.soune.nocopy.dto.file.GoogleVisionSearchResponse;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.exception.NotValidFieldException;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GoogleVisionSearchService {
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private ImageAnnotatorClient visionClient;
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws IOException {
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
||||
|
||||
initVisionClient();
|
||||
}
|
||||
|
||||
private void initVisionClient() throws IOException {
|
||||
try (InputStream credentialsStream = new ClassPathResource("config/google-service-account.json").getInputStream()) {
|
||||
GoogleCredentials credentials = GoogleCredentials.fromStream(credentialsStream);
|
||||
|
||||
ImageAnnotatorSettings settings = ImageAnnotatorSettings.newBuilder()
|
||||
.setCredentialsProvider(() -> credentials)
|
||||
.build();
|
||||
|
||||
this.visionClient = ImageAnnotatorClient.create(settings);
|
||||
}
|
||||
}
|
||||
|
||||
public GoogleVisionSearchResponse searchByFileEntity(String fileId) throws IOException {
|
||||
FileEntity fileEntity = fileEntityRepository.findById(fileId)
|
||||
.orElseThrow(() -> {
|
||||
throw new NotValidFieldException("File not found", new BaseResponse(20007,
|
||||
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
|
||||
Map.of("fileId", fileId)));
|
||||
});
|
||||
|
||||
if (!isImageFile(fileEntity)) {
|
||||
throw new NotValidFieldException("File not image", new BaseResponse(20007,
|
||||
MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(),
|
||||
Map.of("file_type", fileEntity.getMimeType())));
|
||||
}
|
||||
|
||||
byte[] fileBytes;
|
||||
try {
|
||||
fileBytes = readFileFromDisk(fileEntity);
|
||||
} catch (IOException e) {
|
||||
throw new NotValidFieldException("File not found or cannot read file", new BaseResponse(20007,
|
||||
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
|
||||
Map.of("fileId", fileId,
|
||||
"filePath", fileEntity.getFilePath())));
|
||||
}
|
||||
|
||||
return callGoogleVisionApi(fileBytes);
|
||||
}
|
||||
|
||||
private boolean isImageFile(FileEntity fileEntity) {
|
||||
String mimeType = fileEntity.getMimeType();
|
||||
return mimeType != null && mimeType.startsWith("image");
|
||||
}
|
||||
|
||||
private byte[] readFileFromDisk(FileEntity fileEntity) throws IOException {
|
||||
Path filePath = Path.of(fileEntity.getFilePath());
|
||||
|
||||
if (!Files.exists(filePath) || !Files.isReadable(filePath)) {
|
||||
throw new IOException("Cannot read file: " + fileEntity.getFilePath());
|
||||
}
|
||||
|
||||
return Files.readAllBytes(filePath);
|
||||
}
|
||||
|
||||
private GoogleVisionSearchResponse callGoogleVisionApi(byte[] imageBytes) throws IOException {
|
||||
ByteString imgBytes = ByteString.copyFrom(imageBytes);
|
||||
Image image = Image.newBuilder()
|
||||
.setContent(imgBytes)
|
||||
.build();
|
||||
|
||||
Feature feature = Feature.newBuilder()
|
||||
.setType(Feature.Type.WEB_DETECTION)
|
||||
.setMaxResults(20)
|
||||
.build();
|
||||
|
||||
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
|
||||
.addFeatures(feature)
|
||||
.setImage(image)
|
||||
.build();
|
||||
|
||||
BatchAnnotateImagesResponse response = visionClient.batchAnnotateImages(List.of(request));
|
||||
|
||||
if (response.getResponsesCount() == 0) {
|
||||
throw new IOException("Empty response from Google Vision API");
|
||||
}
|
||||
|
||||
AnnotateImageResponse singleResponse = response.getResponses(0);
|
||||
|
||||
if (singleResponse.hasError()) {
|
||||
throw new IOException("Google Vision API error: " + singleResponse.getError().getMessage());
|
||||
}
|
||||
|
||||
return convertToResponse(singleResponse.getWebDetection());
|
||||
}
|
||||
|
||||
private GoogleVisionSearchResponse convertToResponse(WebDetection webDetection) {
|
||||
GoogleVisionSearchResponse response = new GoogleVisionSearchResponse();
|
||||
|
||||
if (webDetection.getBestGuessLabelsCount() > 0) {
|
||||
response.setBestGuessLabels(
|
||||
webDetection.getBestGuessLabelsList().stream()
|
||||
.map(label -> {
|
||||
GoogleVisionSearchResponse.BestGuessLabel dtoLabel =
|
||||
new GoogleVisionSearchResponse.BestGuessLabel();
|
||||
dtoLabel.setLabel(label.getLabel());
|
||||
dtoLabel.setLanguageCode(label.getLanguageCode());
|
||||
return dtoLabel;
|
||||
})
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
if (webDetection.getFullMatchingImagesCount() > 0) {
|
||||
response.setFullMatchingImages(
|
||||
webDetection.getFullMatchingImagesList().stream()
|
||||
.map(img -> {
|
||||
GoogleVisionSearchResponse.ImageResult dtoImg =
|
||||
new GoogleVisionSearchResponse.ImageResult();
|
||||
dtoImg.setUrl(img.getUrl());
|
||||
dtoImg.setScore(img.getScore());
|
||||
return dtoImg;
|
||||
})
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
if (webDetection.getVisuallySimilarImagesCount() > 0) {
|
||||
response.setVisuallySimilarImages(
|
||||
webDetection.getVisuallySimilarImagesList().stream()
|
||||
.map(img -> {
|
||||
GoogleVisionSearchResponse.ImageResult dtoImg =
|
||||
new GoogleVisionSearchResponse.ImageResult();
|
||||
dtoImg.setUrl(img.getUrl());
|
||||
dtoImg.setScore(img.getScore());
|
||||
return dtoImg;
|
||||
})
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
if (webDetection.getPagesWithMatchingImagesCount() > 0) {
|
||||
response.setPagesWithMatchingImages(
|
||||
webDetection.getPagesWithMatchingImagesList().stream()
|
||||
.map(page -> {
|
||||
GoogleVisionSearchResponse.PageResult dtoPage =
|
||||
new GoogleVisionSearchResponse.PageResult();
|
||||
dtoPage.setUrl(page.getUrl());
|
||||
dtoPage.setPageTitle(page.getPageTitle());
|
||||
return dtoPage;
|
||||
})
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
if (webDetection.getPartialMatchingImagesCount() > 0) {
|
||||
response.setPartialMatchingImages(
|
||||
webDetection.getPartialMatchingImagesList().stream()
|
||||
.map(img -> {
|
||||
GoogleVisionSearchResponse.ImageResult dtoImg =
|
||||
new GoogleVisionSearchResponse.ImageResult();
|
||||
dtoImg.setUrl(img.getUrl());
|
||||
dtoImg.setScore(img.getScore());
|
||||
return dtoImg;
|
||||
})
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user