From 674108744773128111e51b8aeccbcd87e930afdf Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Sun, 2 Aug 2026 16:42:42 +0200 Subject: [PATCH 1/7] chore: new field for image and github secrect --- .github/workflows/ci.yml | 1 + src/main/resources/application.properties | 6 +++++- src/main/resources/db/migration/V5__add_tags_to_images.sql | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V5__add_tags_to_images.sql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66cedcb..4a787ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: LOKI_URL: http://localhost:3100 LOKI_USERNAME: dummy LOKI_API_TOKEN: dummy + GITHUB_MODELS_TOKEN: dummy build-and-push: runs-on: ubuntu-latest diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2be0e11..12579e4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -16,4 +16,8 @@ b2.bucket=${B2_BUCKET_NAME} spring.servlet.multipart.max-file-size=15MB -spring.servlet.multipart.max-request-size=15MB \ No newline at end of file +spring.servlet.multipart.max-request-size=15MB + + + +github.models.token=${GITHUB_MODELS_TOKEN} \ No newline at end of file diff --git a/src/main/resources/db/migration/V5__add_tags_to_images.sql b/src/main/resources/db/migration/V5__add_tags_to_images.sql new file mode 100644 index 0000000..cfbbe71 --- /dev/null +++ b/src/main/resources/db/migration/V5__add_tags_to_images.sql @@ -0,0 +1,3 @@ +ALTER TABLE images ADD COLUMN tags JSONB; + +CREATE INDEX idx_images_tags ON images USING GIN (tags); \ No newline at end of file From f81431f0461c6838f98c96b40db900ddb5203f5d Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Sun, 2 Aug 2026 18:17:26 +0200 Subject: [PATCH 2/7] fix: change to gemini --- .github/workflows/ci.yml | 2 +- .../taglens/configuration/GeminiConfig.java | 21 ++++++++++ .../taglens/configuration/SecurityConfig.java | 1 + .../taglens/controller/TestController.java | 20 ++++++++++ .../taglens/service/TaggingService.java | 39 +++++++++++++++++++ src/main/resources/application.properties | 3 +- 6 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/main/java/dev/barboud/taglens/configuration/GeminiConfig.java create mode 100644 src/main/java/dev/barboud/taglens/controller/TestController.java create mode 100644 src/main/java/dev/barboud/taglens/service/TaggingService.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a787ba..c5241ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: LOKI_URL: http://localhost:3100 LOKI_USERNAME: dummy LOKI_API_TOKEN: dummy - GITHUB_MODELS_TOKEN: dummy + GEMINI_API_KEY: dummy build-and-push: runs-on: ubuntu-latest diff --git a/src/main/java/dev/barboud/taglens/configuration/GeminiConfig.java b/src/main/java/dev/barboud/taglens/configuration/GeminiConfig.java new file mode 100644 index 0000000..a8b9c43 --- /dev/null +++ b/src/main/java/dev/barboud/taglens/configuration/GeminiConfig.java @@ -0,0 +1,21 @@ +package dev.barboud.taglens.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +@Configuration +public class GeminiConfig { + + @Value("${gemini.api.key}") + private String apiKey; + + @Bean + public RestClient geminiClient() { + return RestClient.builder() + .baseUrl("https://generativelanguage.googleapis.com/v1beta") + .defaultHeader("Content-Type", "application/json") + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/barboud/taglens/configuration/SecurityConfig.java b/src/main/java/dev/barboud/taglens/configuration/SecurityConfig.java index ebb3a05..f9446d9 100644 --- a/src/main/java/dev/barboud/taglens/configuration/SecurityConfig.java +++ b/src/main/java/dev/barboud/taglens/configuration/SecurityConfig.java @@ -23,6 +23,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/auth/login", "/auth/register").permitAll() // login & register: public .requestMatchers("/", "/*.html", "/*.css", "/*.js").permitAll() .requestMatchers("/actuator/**").permitAll() + .requestMatchers("/test/**").permitAll() .anyRequest().authenticated() // everything else: must be logged in ) .addFilterBefore(sessionAuthFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/dev/barboud/taglens/controller/TestController.java b/src/main/java/dev/barboud/taglens/controller/TestController.java new file mode 100644 index 0000000..4bbe980 --- /dev/null +++ b/src/main/java/dev/barboud/taglens/controller/TestController.java @@ -0,0 +1,20 @@ +package dev.barboud.taglens.controller; + +import dev.barboud.taglens.service.TaggingService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test") +@RequiredArgsConstructor +public class TestController { + + private final TaggingService taggingService; + + @GetMapping("/ai") + public String testAi() { + return taggingService.testConnection(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/barboud/taglens/service/TaggingService.java b/src/main/java/dev/barboud/taglens/service/TaggingService.java new file mode 100644 index 0000000..5188246 --- /dev/null +++ b/src/main/java/dev/barboud/taglens/service/TaggingService.java @@ -0,0 +1,39 @@ +package dev.barboud.taglens.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class TaggingService { + + private final RestClient geminiClient; + + @Value("${gemini.api.key}") + private String apiKey; + + public String testConnection() { + Map body = Map.of( + "contents", new Object[] { + Map.of("parts", new Object[] { + Map.of("text", "ماهي عاصمة السعودية؟") + }) + }, + "generationConfig", Map.of( + "thinkingConfig", Map.of( + "thinkingLevel", "minimal" + ) + ) + ); + + return geminiClient.post() + .uri("/models/gemini-3.6-flash:generateContent?key=" + apiKey) + .body(body) + .retrieve() + .body(String.class); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 12579e4..7e3aeb6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -19,5 +19,4 @@ spring.servlet.multipart.max-file-size=15MB spring.servlet.multipart.max-request-size=15MB - -github.models.token=${GITHUB_MODELS_TOKEN} \ No newline at end of file +gemini.api.key=${GEMINI_API_KEY} \ No newline at end of file From 55771c729d7ecf20c603b96393ef346e4404fd86 Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Sun, 2 Aug 2026 18:40:32 +0200 Subject: [PATCH 3/7] feat: Tagging Service work and tested --- .../taglens/controller/TestController.java | 11 ++-- .../taglens/service/TaggingService.java | 59 ++++++++++++++++--- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/main/java/dev/barboud/taglens/controller/TestController.java b/src/main/java/dev/barboud/taglens/controller/TestController.java index 4bbe980..1533dce 100644 --- a/src/main/java/dev/barboud/taglens/controller/TestController.java +++ b/src/main/java/dev/barboud/taglens/controller/TestController.java @@ -2,9 +2,12 @@ import dev.barboud.taglens.service.TaggingService; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import tools.jackson.databind.JsonNode; @RestController @RequestMapping("/test") @@ -13,8 +16,8 @@ public class TestController { private final TaggingService taggingService; - @GetMapping("/ai") - public String testAi() { - return taggingService.testConnection(); + @PostMapping("/ai-image") + public JsonNode testAiImage(@RequestParam("file") MultipartFile file) throws Exception { + return taggingService.generateTags(file.getBytes(), file.getContentType()); } } \ No newline at end of file diff --git a/src/main/java/dev/barboud/taglens/service/TaggingService.java b/src/main/java/dev/barboud/taglens/service/TaggingService.java index 5188246..ef5548c 100644 --- a/src/main/java/dev/barboud/taglens/service/TaggingService.java +++ b/src/main/java/dev/barboud/taglens/service/TaggingService.java @@ -4,7 +4,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; +import software.amazon.awssdk.thirdparty.jackson.core.type.TypeReference; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import java.util.Base64; +import java.util.List; import java.util.Map; @Service @@ -12,22 +17,46 @@ public class TaggingService { private final RestClient geminiClient; + private final ObjectMapper objectMapper = new ObjectMapper(); @Value("${gemini.api.key}") private String apiKey; - public String testConnection() { + public String generateTagsRaw(byte[] imageBytes, String contentType) { + String base64Image = Base64.getEncoder().encodeToString(imageBytes); + + String prompt = """ + You are an expert AI image analysis system. Your job is to analyze the provided image and generate a valid JSON object containing tags separated into three distinct categories: "objects", "tags", and "colors". + ### Guidelines & Rules: + 1. **objects**: + - List physical, tangible items present in the image (e.g., "tree", "car", "cloud", "sun", "boat"). + - Keep names clear and concise in lowercase English. + 2. **tags**: + - List non-object descriptors describing the scene, setting, time of day, weather, mood, style, or atmosphere (e.g., "sunset", "tropical", "silhouette", "reflection on water"). + 3. **colors**: + - Identify up to 3 of the most prominent colors in the image. + - You MUST ONLY select from the 11 basic colors in English: ["black", "blue", "brown", "gray", "green", "orange", "pink", "purple", "red", "white", "yellow"]. + ### Output Requirements: + - Respond strictly with valid JSON. + - Do NOT wrap the output in markdown extra text or explanations. + - Follow this exact schema format: + { + "objects": ["string"], + "tags": ["string"], + "colors": ["string"] + } + """; + Map body = Map.of( "contents", new Object[] { Map.of("parts", new Object[] { - Map.of("text", "ماهي عاصمة السعودية؟") + Map.of("text", prompt), + Map.of("inline_data", Map.of( + "mime_type", contentType, + "data", base64Image + )) }) - }, - "generationConfig", Map.of( - "thinkingConfig", Map.of( - "thinkingLevel", "minimal" - ) - ) + } ); return geminiClient.post() @@ -36,4 +65,18 @@ public String testConnection() { .retrieve() .body(String.class); } + + public JsonNode generateTags(byte[] imageBytes, String contentType) { + String rawResponse = generateTagsRaw(imageBytes, contentType); + + JsonNode root = objectMapper.readTree(rawResponse); + String text = root.path("candidates").get(0) + .path("content").path("parts").get(0) + .path("text").asText(); + + text = text.replaceAll("```json|```", "").trim(); + + return objectMapper.readTree(text); + } + } \ No newline at end of file From fb0727be014ec894cd86b393cde0be5ff5a32fe9 Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Sun, 2 Aug 2026 18:56:05 +0200 Subject: [PATCH 4/7] feat: asyn enable --- .../java/dev/barboud/taglens/TagLensApplication.java | 2 ++ .../dev/barboud/taglens/service/TaggingService.java | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/barboud/taglens/TagLensApplication.java b/src/main/java/dev/barboud/taglens/TagLensApplication.java index 565ac90..3d29a2b 100644 --- a/src/main/java/dev/barboud/taglens/TagLensApplication.java +++ b/src/main/java/dev/barboud/taglens/TagLensApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication +@EnableAsync public class TagLensApplication { public static void main(String[] args) { diff --git a/src/main/java/dev/barboud/taglens/service/TaggingService.java b/src/main/java/dev/barboud/taglens/service/TaggingService.java index ef5548c..5e1235e 100644 --- a/src/main/java/dev/barboud/taglens/service/TaggingService.java +++ b/src/main/java/dev/barboud/taglens/service/TaggingService.java @@ -1,17 +1,19 @@ package dev.barboud.taglens.service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; -import software.amazon.awssdk.thirdparty.jackson.core.type.TypeReference; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import java.util.Base64; -import java.util.List; import java.util.Map; +import java.util.UUID; +@Slf4j @Service @RequiredArgsConstructor public class TaggingService { @@ -79,4 +81,10 @@ public JsonNode generateTags(byte[] imageBytes, String contentType) { return objectMapper.readTree(text); } + @Async + public void tagImageAsync(UUID imageId, byte[] imageBytes, String contentType) { + JsonNode tags = generateTags(imageBytes, contentType); + log.info("Tags generated for image={}, tags={}", imageId, tags); + } + } \ No newline at end of file From d97ebf07487e6124f2d65a572325df6dcae355b3 Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Sun, 2 Aug 2026 19:28:53 +0200 Subject: [PATCH 5/7] feat: updateTags in image repository --- .../taglens/configuration/SecurityConfig.java | 1 - .../taglens/controller/TestController.java | 23 ------------------- .../taglens/repository/ImageRepository.java | 9 ++++++++ 3 files changed, 9 insertions(+), 24 deletions(-) delete mode 100644 src/main/java/dev/barboud/taglens/controller/TestController.java diff --git a/src/main/java/dev/barboud/taglens/configuration/SecurityConfig.java b/src/main/java/dev/barboud/taglens/configuration/SecurityConfig.java index f9446d9..ebb3a05 100644 --- a/src/main/java/dev/barboud/taglens/configuration/SecurityConfig.java +++ b/src/main/java/dev/barboud/taglens/configuration/SecurityConfig.java @@ -23,7 +23,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/auth/login", "/auth/register").permitAll() // login & register: public .requestMatchers("/", "/*.html", "/*.css", "/*.js").permitAll() .requestMatchers("/actuator/**").permitAll() - .requestMatchers("/test/**").permitAll() .anyRequest().authenticated() // everything else: must be logged in ) .addFilterBefore(sessionAuthFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/dev/barboud/taglens/controller/TestController.java b/src/main/java/dev/barboud/taglens/controller/TestController.java deleted file mode 100644 index 1533dce..0000000 --- a/src/main/java/dev/barboud/taglens/controller/TestController.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.barboud.taglens.controller; - -import dev.barboud.taglens.service.TaggingService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; -import tools.jackson.databind.JsonNode; - -@RestController -@RequestMapping("/test") -@RequiredArgsConstructor -public class TestController { - - private final TaggingService taggingService; - - @PostMapping("/ai-image") - public JsonNode testAiImage(@RequestParam("file") MultipartFile file) throws Exception { - return taggingService.generateTags(file.getBytes(), file.getContentType()); - } -} \ No newline at end of file diff --git a/src/main/java/dev/barboud/taglens/repository/ImageRepository.java b/src/main/java/dev/barboud/taglens/repository/ImageRepository.java index 3edf766..c28e9f8 100644 --- a/src/main/java/dev/barboud/taglens/repository/ImageRepository.java +++ b/src/main/java/dev/barboud/taglens/repository/ImageRepository.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; +import tools.jackson.databind.JsonNode; import java.util.List; import java.util.Optional; @@ -81,4 +82,12 @@ public List findImagesByUserId(UUID userId) { return results; } + public void updateTags(UUID imageId, JsonNode tags) { + String json = tags.toString(); + jdbcTemplate.update( + "UPDATE images SET tags = ?::jsonb WHERE id = ?", + json, imageId + ); + } + } \ No newline at end of file From c9ba0465c305e59078869f431992f93369ca88cc Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Sun, 2 Aug 2026 19:41:35 +0200 Subject: [PATCH 6/7] refator: use Map<> --- .../barboud/taglens/repository/ImageRepository.java | 9 +++++---- .../dev/barboud/taglens/service/TaggingService.java | 12 ++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/dev/barboud/taglens/repository/ImageRepository.java b/src/main/java/dev/barboud/taglens/repository/ImageRepository.java index c28e9f8..127a835 100644 --- a/src/main/java/dev/barboud/taglens/repository/ImageRepository.java +++ b/src/main/java/dev/barboud/taglens/repository/ImageRepository.java @@ -1,13 +1,13 @@ package dev.barboud.taglens.repository; -import dev.barboud.taglens.dto.image.ImageResponseDto; import dev.barboud.taglens.model.Image; import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; -import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -16,6 +16,7 @@ public class ImageRepository { private final JdbcTemplate jdbcTemplate; + private final ObjectMapper objectMapper; public void saveImage(Image image) { @@ -82,8 +83,8 @@ public List findImagesByUserId(UUID userId) { return results; } - public void updateTags(UUID imageId, JsonNode tags) { - String json = tags.toString(); + public void updateImageTags(UUID imageId, Map tags) { + String json = objectMapper.writeValueAsString(tags); jdbcTemplate.update( "UPDATE images SET tags = ?::jsonb WHERE id = ?", json, imageId diff --git a/src/main/java/dev/barboud/taglens/service/TaggingService.java b/src/main/java/dev/barboud/taglens/service/TaggingService.java index 5e1235e..1b705a1 100644 --- a/src/main/java/dev/barboud/taglens/service/TaggingService.java +++ b/src/main/java/dev/barboud/taglens/service/TaggingService.java @@ -1,11 +1,13 @@ package dev.barboud.taglens.service; +import dev.barboud.taglens.repository.ImageRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; +import tools.jackson.core.type.TypeReference; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; @@ -20,6 +22,7 @@ public class TaggingService { private final RestClient geminiClient; private final ObjectMapper objectMapper = new ObjectMapper(); + private final ImageRepository imageRepository; @Value("${gemini.api.key}") private String apiKey; @@ -68,7 +71,7 @@ public String generateTagsRaw(byte[] imageBytes, String contentType) { .body(String.class); } - public JsonNode generateTags(byte[] imageBytes, String contentType) { + public Map generateTags(byte[] imageBytes, String contentType) { String rawResponse = generateTagsRaw(imageBytes, contentType); JsonNode root = objectMapper.readTree(rawResponse); @@ -78,13 +81,14 @@ public JsonNode generateTags(byte[] imageBytes, String contentType) { text = text.replaceAll("```json|```", "").trim(); - return objectMapper.readTree(text); + return objectMapper.readValue(text, new TypeReference>() {}); } @Async public void tagImageAsync(UUID imageId, byte[] imageBytes, String contentType) { - JsonNode tags = generateTags(imageBytes, contentType); - log.info("Tags generated for image={}, tags={}", imageId, tags); + Map tags = generateTags(imageBytes, contentType); + imageRepository.updateImageTags(imageId, tags); + log.info("Tags saved for image={}, tags={}", imageId, tags); } } \ No newline at end of file From 7da9715704a73df11ae7077944d683c7ad746956 Mon Sep 17 00:00:00 2001 From: Salem Ba-Rabuod Date: Sun, 2 Aug 2026 20:14:29 +0200 Subject: [PATCH 7/7] feat: save tags generated --- .../taglens/controller/ImageController.java | 8 +++----- .../taglens/dto/image/ImageResponseDto.java | 2 ++ .../java/dev/barboud/taglens/model/Image.java | 6 ++++++ .../taglens/repository/ImageRepository.java | 13 +++++++++++-- .../dev/barboud/taglens/service/ImageService.java | 15 +++++++-------- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/main/java/dev/barboud/taglens/controller/ImageController.java b/src/main/java/dev/barboud/taglens/controller/ImageController.java index a6b976f..0912d85 100644 --- a/src/main/java/dev/barboud/taglens/controller/ImageController.java +++ b/src/main/java/dev/barboud/taglens/controller/ImageController.java @@ -23,16 +23,14 @@ public class ImageController { @PostMapping("/upload") - public ResponseEntity uploadImage( + public ResponseEntity uploadImage( @RequestParam("file") MultipartFile file, @CookieValue("session_id") UUID sessionId ) throws Exception { - ImageResponseDto imageResponseDto = imageService.uploadImage(file, sessionId); + imageService.uploadImage(file, sessionId); - return ResponseEntity - .status(HttpStatus.CREATED) - .body(imageResponseDto); + return ResponseEntity.noContent().build(); } @GetMapping("/{id}") diff --git a/src/main/java/dev/barboud/taglens/dto/image/ImageResponseDto.java b/src/main/java/dev/barboud/taglens/dto/image/ImageResponseDto.java index 9c8375f..63fa053 100644 --- a/src/main/java/dev/barboud/taglens/dto/image/ImageResponseDto.java +++ b/src/main/java/dev/barboud/taglens/dto/image/ImageResponseDto.java @@ -3,6 +3,7 @@ import lombok.AllArgsConstructor; import lombok.Data; +import java.util.Map; import java.util.UUID; @Data @@ -12,4 +13,5 @@ public class ImageResponseDto { String contentType; Long fileSize; private String storageKey; + Map tags; } diff --git a/src/main/java/dev/barboud/taglens/model/Image.java b/src/main/java/dev/barboud/taglens/model/Image.java index 15071ce..1d37512 100644 --- a/src/main/java/dev/barboud/taglens/model/Image.java +++ b/src/main/java/dev/barboud/taglens/model/Image.java @@ -1,11 +1,16 @@ package dev.barboud.taglens.model; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.Instant; +import java.util.Map; import java.util.UUID; @Data +@AllArgsConstructor +@NoArgsConstructor public class Image { private UUID id; private UUID userId; @@ -13,4 +18,5 @@ public class Image { private String contentType; private Long fileSize; private Instant createdAt; + private Map tags; } diff --git a/src/main/java/dev/barboud/taglens/repository/ImageRepository.java b/src/main/java/dev/barboud/taglens/repository/ImageRepository.java index 127a835..7788d5d 100644 --- a/src/main/java/dev/barboud/taglens/repository/ImageRepository.java +++ b/src/main/java/dev/barboud/taglens/repository/ImageRepository.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; +import tools.jackson.core.type.TypeReference; import tools.jackson.databind.ObjectMapper; import java.util.List; @@ -18,6 +19,12 @@ public class ImageRepository { private final JdbcTemplate jdbcTemplate; private final ObjectMapper objectMapper; + + private Map parseTags(String tagsJson) { + if (tagsJson == null) return null; + return objectMapper.readValue(tagsJson, new TypeReference>() {}); + } + public void saveImage(Image image) { jdbcTemplate.update(""" @@ -36,7 +43,7 @@ public void saveImage(Image image) { public Optional findImageById(UUID imageId) { String sql = """ - SELECT id, user_id, storage_key, content_type, file_size, created_at + SELECT id, user_id, storage_key, content_type, file_size, created_at, tags FROM images WHERE id = ? """; @@ -49,6 +56,7 @@ public Optional findImageById(UUID imageId) { image.setContentType(rs.getString("content_type")); image.setFileSize(rs.getLong("file_size")); image.setCreatedAt(rs.getTimestamp("created_at").toInstant()); + image.setTags(parseTags(rs.getString("tags"))); return image; }, imageId); @@ -65,7 +73,7 @@ public void deleteImage(UUID imageId) { public List findImagesByUserId(UUID userId) { String sql = """ - SELECT id, user_id, storage_key, content_type, file_size, created_at + SELECT id, user_id, storage_key, content_type, file_size, created_at, tags FROM images WHERE user_id = ? """; @@ -77,6 +85,7 @@ public List findImagesByUserId(UUID userId) { image.setContentType(rs.getString("content_type")); image.setFileSize(rs.getLong("file_size")); image.setCreatedAt(rs.getTimestamp("created_at").toInstant()); + image.setTags(parseTags(rs.getString("tags"))); return image; }, userId); diff --git a/src/main/java/dev/barboud/taglens/service/ImageService.java b/src/main/java/dev/barboud/taglens/service/ImageService.java index 99b399d..14c3405 100644 --- a/src/main/java/dev/barboud/taglens/service/ImageService.java +++ b/src/main/java/dev/barboud/taglens/service/ImageService.java @@ -20,10 +20,11 @@ public class ImageService { private final SessionRepository sessionRepository; private final ImageRepository imageRepository; private final StorageService storageService; + private final TaggingService taggingService; private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB - public ImageResponseDto uploadImage( + public void uploadImage( MultipartFile file, UUID sessionId ) throws Exception { @@ -67,12 +68,9 @@ public ImageResponseDto uploadImage( // 7. Save image metadata in Database imageRepository.saveImage(image); - return new ImageResponseDto( - image.getId(), - image.getContentType(), - image.getFileSize(), - image.getStorageKey() - ); + // 8. Generate Ai Tags + taggingService.tagImageAsync(image.getId(), file.getBytes(), file.getContentType()); + } @@ -114,7 +112,8 @@ public List getMyImages(UUID sessionId){ image.getId(), image.getContentType(), image.getFileSize(), - image.getStorageKey() + image.getStorageKey(), + image.getTags() )) .toList();