Mise en œuvre de Qwen3-ASR-0.6B en Production : Guide d'intégration Spring Boot avec optimisation des ressources
La reconnaissance vocale (ASR) révolutionne l'expérience utilisateur dans les applications d'entreprise. Le modèle Qwen3-ASR-0.6B se distingue par son excellent rapport performance/efficacité, ce qui en fait un choix privilégié pour un déploiement en production. Cet article détaille l'intégration efficace de ce puissant service ASR dans un projet Spring Boot.
- Préparation de l'environnement et configuration des dépendances
Avant de procéder à l'intégration, il est essentiel de préparer l'environnement de développement. Bien que Qwen3-ASR-0.6B soit relativement léger par rapport aux grands modèles linguistiques, il requiert des spécifications matérielles minimales.
Assurez-vous que votre environnement respecte les prérequis suivants :
- JDK 17 ou version supérieure
- Spring Boot 3.x
- Python 3.8+ (pour l'inférence du modèle)
- CUDA 11.7+ (pour l'accélération GPU, si disponible)
- Au minimum 8 Go de RAM (16 Go sont recommandés)
Ajoutez les dépendances nécessaires au fichier pom.xml de votre projet Spring Boot :
<dependencies>
<!-- Module Spring Boot Web pour les API REST -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Support AOP pour les opérations asynchrones et la surveillance -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Bibliothèque pour la manipulation audio -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.0</version>
</dependency>
<!-- Intégration pour les métriques Micrometer -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
</dependencies>
Pour l'environnement Python, créez un fichier requirements.txt :
# requirements.txt
qwen-asr==0.1.0
torch==2.3.0
numpy==1.24.0
librosa==0.10.0
fastapi==0.110.0
uvicorn==0.29.0
- Conception et implémentation de la couche de service principale
2.1. Encapsulation du service d'inférence Python
Nous allons d'abord créer un service FastAPI pour encapsuler les capacités d'inférence de Qwen3-ASR :
# asr_inference_api.py
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.concurrency import run_in_threadpool
import torch
from qwen_asr import Qwen3ASRModel
import librosa
import io
import logging
# Configuration du logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
api_server = FastAPI(title="Service d'Inférence Qwen3-ASR")
# Instance globale du modèle pour un chargement unique
global_asr_model = None
@api_server.on_event("startup")
async def initialize_asr_engine():
"""Charge le modèle ASR au démarrage du service."""
global global_asr_model
try:
logger.info("Tentative de chargement du modèle Qwen3-ASR-0.6B...")
global_asr_model = Qwen3ASRModel.from_pretrained(
"Qwen/Qwen3-ASR-0.6B",
dtype=torch.bfloat16,
device_map="auto", # Détecte automatiquement le meilleur appareil (GPU/CPU)
max_inference_batch_size=16,
max_new_tokens=512
)
logger.info("Modèle Qwen3-ASR-0.6B chargé avec succès.")
except Exception as e:
logger.error(f"Échec du chargement du modèle ASR : {e}", exc_info=True)
raise RuntimeError(f"Impossible de charger le modèle ASR : {e}")
@api_server.post("/recognize-speech")
async def process_audio_for_transcription(audio_file: UploadFile = File(...)):
"""Traite un fichier audio pour la transcription vocale."""
if not global_asr_model:
raise HTTPException(status_code=503, detail="Le service de reconnaissance vocale n'est pas prêt. Le modèle n'est pas chargé.")
try:
logger.info(f"Réception du fichier audio : {audio_file.filename}")
# Lire le contenu du fichier audio
audio_content_bytes = await audio_file.read()
# Charger et rééchantillonner l'audio avec librosa
audio_buffer = io.BytesIO(audio_content_bytes)
audio_waveform, sample_rate = librosa.load(audio_buffer, sr=16000) # Cible 16kHz
# Exécuter la reconnaissance vocale dans un thread séparé
recognition_result = await run_in_threadpool(
global_asr_model.transcribe,
audio=audio_waveform,
sample_rate=sample_rate,
language=None # Détection automatique de la langue
)
if not recognition_result:
raise HTTPException(status_code=500, detail="Aucun résultat de transcription n'a été retourné.")
# Retourner les informations de transcription
return {
"transcribedText": recognition_result[0].text,
"detectedLanguage": recognition_result[0].language,
"confidenceScore": getattr(recognition_result[0], 'confidence', 0.9)
}
except Exception as e:
logger.error(f"Échec de la transcription pour {audio_file.filename}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Erreur lors de la reconnaissance vocale : {str(e)}")
if __name__ == "__main__":
import uvicorn
# Lance le serveur FastAPI sur le port 8000
uvicorn.run(api_server, host="0.0.0.0", port=8000)
2.2. Intégration du service Spring Boot
Côté Java, nous allons créer une classe de service correspondante pour interroger le service Python :
// SpeechRecognitionService.java
package com.example.asr.service;
import com.example.asr.model.RecognitionResult;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.*;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
@Service
@Slf4j
public class SpeechRecognitionService {
@Value("${asr.api.base-url:http://localhost:8000}")
private String asrApiBaseUrl;
private final RestTemplate restApiClient;
public SpeechRecognitionService(RestTemplateBuilder restTemplateBuilder) {
this.restApiClient = restTemplateBuilder.build();
}
@Async("asrTaskExecutor") // Utilise un ThreadPoolExecutor dédié
public CompletableFuture<RecognitionResult> submitAudioForRecognition(MultipartFile audioFile) {
return CompletableFuture.supplyAsync(() -> {
try {
// Configure les en-têtes pour un appel multipart/form-data
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// Crée le corps de la requête avec le fichier audio
MultiValueMap<String, Object> requestBody = new LinkedMultiValueMap<>();
requestBody.add("audio_file", new InputStreamResource(audioFile.getInputStream()) {
@Override
public String getFilename() {
return audioFile.getOriginalFilename();
}
});
// Construit l'entité de la requête HTTP
HttpEntity<MultiValueMap<String, Object>> requestEntity =
new HttpEntity<>(requestBody, headers);
// Envoie la requête POST au service d'inférence Python
ResponseEntity<RecognitionResult> response = restApiClient.postForEntity(
asrApiBaseUrl + "/recognize-speech",
requestEntity,
RecognitionResult.class
);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
log.info("Transcription réussie pour le fichier {}", audioFile.getOriginalFilename());
return response.getBody();
} else {
log.warn("La requête ASR a échoué avec le code {}", response.getStatusCode());
throw new RuntimeException("Service de reconnaissance vocale indisponible ou erreur interne.");
}
} catch (ResourceAccessException e) {
log.error("Échec de connexion au service ASR externe : {}", e.getMessage());
throw new RuntimeException("Le service de reconnaissance vocale est injoignable.", e);
} catch (IOException e) {
log.error("Erreur de lecture du fichier audio : {}", audioFile.getOriginalFilename(), e);
throw new RuntimeException("Impossible de lire le fichier audio fourni.", e);
} catch (Exception e) {
log.error("Erreur inattendue lors de la transcription : {}", e.getMessage(), e);
throw new RuntimeException("Une erreur inattendue est survenue lors de la transcription.", e);
}
});
}
}
// RecognitionResult.java (Modèle de données pour le résultat de la reconnaissance)
package com.example.asr.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RecognitionResult {
private String transcribedText;
private String detectedLanguage;
private Double confidenceScore;
private Long processingDurationMillis; // Ajouté pour le suivi des performances
}
- Contrôle de la concurrence et optimisation des performances
3.1. Configuraton des pools de connexion et de threads
Dans une application d'entreprise, une configuration adéquate des pools de ressources est essentielle :
# application.yml
spring:
task:
execution:
# Configuration du pool de threads pour les tâches asynchrones Spring
pool:
core-size: 20
max-size: 50
queue-capacity: 1000
keep-alive: 60s
thread-name-prefix: async-task-
datasource: # Exemple pour HikariCP, si vous utilisez une base de données
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# Configuration spécifique du service ASR
asr:
api:
base-url: http://localhost:8000
request-timeout: 45000 # Délai d'attente pour une requête ASR (en ms)
Classes de configuration correspondantes :
// AsyncThreadPoolConfig.java
package com.example.asr.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
@Bean("asrTaskExecutor")
public TaskExecutor configureAsrTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(25);
executor.setQueueCapacity(200); // Capacité de la file d'attente pour les tâches ASR
executor.setThreadNamePrefix("asr-processing-");
// Politique de rejet: L'appelant exécute la tâche directement
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
// RestClientConfig.java
package com.example.asr.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
@Configuration
public class RestClientConfig {
@Value("${asr.api.request-timeout:45000}")
private int asrRequestTimeoutMillis;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(5000)) // Délai d'attente pour l'établissement de la connexion
.setReadTimeout(Duration.ofMillis(asrRequestTimeoutMillis)) // Délai d'attente pour la lecture des données
.build();
}
}
3.2. Traitement par lots et optimisation du streaming
Pour gérer de grands volumes de fichiers audio, une fonctionnalité de traitement par lots est bénéfique :
// ConcurrentSpeechRecognitionService.java
package com.example.asr.service;
import com.example.asr.model.RecognitionResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class ConcurrentSpeechRecognitionService {
private final SpeechRecognitionService speechRecognitionService;
private final ExecutorService batchProcessingPool = Executors.newFixedThreadPool(15); // Pool pour le traitement par lots
@Autowired
public ConcurrentSpeechRecognitionService(SpeechRecognitionService speechRecognitionService) {
this.speechRecognitionService = speechRecognitionService;
}
public List<CompletableFuture<RecognitionResult>> processMultipleAudioFiles(List<MultipartFile> files) {
List<CompletableFuture<RecognitionResult>> futuresList = new ArrayList<>();
for (MultipartFile file : files) {
CompletableFuture<RecognitionResult> future = CompletableFuture.supplyAsync(() -> {
try {
// Chaque fichier est traité individuellement de manière asynchrone
return speechRecognitionService.submitAudioForRecognition(file).get(60, TimeUnit.SECONDS); // Délai par fichier
} catch (Exception e) {
log.warn("Échec de la transcription pour le fichier : {}", file.getOriginalFilename(), e);
// Retourne un résultat d'erreur pour ce fichier spécifique
return new RecognitionResult("", "error", 0.0, null);
}
}, batchProcessingPool); // Exécute les tâches de transcription dans le pool dédié au batch
futuresList.add(future);
}
return futuresList;
}
// Interface de traitement en streaming pour des flux d'audio potentiellement longs
public Flux<RecognitionResult> streamAudioForRecognition(Flux<MultipartFile> fileStream) {
return fileStream
.flatMap(file -> Mono.fromFuture(() ->
speechRecognitionService.submitAudioForRecognition(file))
)
.onErrorContinue((error, item) ->
log.warn("Erreur rencontrée lors du traitement en streaming, ignoration de l'élément : {}", item, error)
);
}
}
- Gestion des exceptions et surveillance
4.1. Gestion globale des exceptions
// GlobalApiExceptionHandler.java
package com.example.asr.exception;
import com.example.asr.model.ApiResponseError;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.server.ResponseStatusException;
import java.util.concurrent.TimeoutException;
@RestControllerAdvice
@Slf4j
public class GlobalApiExceptionHandler {
@ExceptionHandler(TimeoutException.class)
public ResponseEntity<ApiResponseError> handleServiceTimeout(TimeoutException ex) {
log.warn("Une requête vers le service ASR a expiré.", ex);
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
.body(new ApiResponseError("RECOGNITION_TIMEOUT", "Le service de reconnaissance vocale a dépassé le délai imparti."));
}
@ExceptionHandler(ResourceAccessException.class)
public ResponseEntity<ApiResponseError> handleExternalServiceUnavailable(ResourceAccessException ex) {
log.error("Impossible de joindre le service de reconnaissance vocale.", ex);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new ApiResponseError("ASR_SERVICE_UNAVAILABLE", "Le service de reconnaissance vocale n'est pas accessible pour le moment."));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponseError> handleInvalidInput(IllegalArgumentException ex) {
log.warn("Argument invalide fourni : {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ApiResponseError("INVALID_INPUT", ex.getMessage()));
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<ApiResponseError> handleResponseStatusException(ResponseStatusException ex) {
log.error("Erreur de statut de réponse : {}", ex.getReason());
return ResponseEntity.status(ex.getStatusCode())
.body(new ApiResponseError("API_ERROR", ex.getReason()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponseError> handleGeneralException(Exception ex) {
log.error("Une erreur interne inattendue est survenue.", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ApiResponseError("INTERNAL_SERVER_ERROR", "Une erreur interne est survenue."));
}
}
// ApiResponseError.java (Modèle d'erreur)
package com.example.asr.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponseError {
private String errorCode;
private String errorMessage;
private long timestamp = System.currentTimeMillis();
}
4.2. Surveillance des performances et collecte de métriques
// SpeechRecognitionMetricsCollector.java
package com.example.asr.monitoring;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class SpeechRecognitionMetricsCollector {
private final Counter successfulRecognitionCount;
private final Counter failedRecognitionCount;
private final Timer recognitionResponseTime;
public SpeechRecognitionMetricsCollector(MeterRegistry meterRegistry) {
this.successfulRecognitionCount = meterRegistry.counter("asr.requests.success.total", "operation", "recognition");
this.failedRecognitionCount = meterRegistry.counter("asr.requests.failure.total", "operation", "recognition");
this.recognitionResponseTime = meterRegistry.timer("asr.response.duration", "operation", "recognition");
}
public void registerSuccess(long durationMillis) {
successfulRecognitionCount.increment();
recognitionResponseTime.record(durationMillis, TimeUnit.MILLISECONDS);
}
public void registerFailure() {
failedRecognitionCount.increment();
}
// Méthodes pour obtenir des métriques si nécessaire
public double getCurrentSuccessRate() {
double totalRequests = successfulRecognitionCount.count() + failedRecognitionCount.count();
return totalRequests > 0 ? successfulRecognitionCount.count() / totalRequests : 1.0;
}
}
Intégration de la surveillance dans le service ASR via AOP :
// MonitoringAspect.java
package com.example.asr.monitoring;
import com.example.asr.service.SpeechRecognitionService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
public class MonitoringAspect {
private final SpeechRecognitionMetricsCollector metricsCollector;
@Autowired
public MonitoringAspect(SpeechRecognitionMetricsCollector metricsCollector) {
this.metricsCollector = metricsCollector;
}
@Around("execution(* com.example.asr.service.SpeechRecognitionService.submitAudioForRecognition(..))")
public Object measureAsrRequestPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
metricsCollector.registerSuccess(duration);
return result;
} catch (Exception e) {
metricsCollector.registerFailure();
log.error("Échec de la surveillance ASR pour {}: {}", joinPoint.getSignature().getName(), e.getMessage());
throw e;
}
}
}
- Exemple de contrôleur REST complet
Enfin, nous créons un contrôleur REST pour exposer les points d'API de reconnaissance vocale :
// SpeechRecognitionApiController.java
package com.example.asr.controller;
import com.example.asr.model.RecognitionResult;
import com.example.asr.service.ConcurrentSpeechRecognitionService;
import com.example.asr.service.SpeechRecognitionService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/speech-recognition")
@Validated
@Slf4j
public class SpeechRecognitionApiController {
private final SpeechRecognitionService singleAsrService;
private final ConcurrentSpeechRecognitionService batchAsrService;
@Autowired
public SpeechRecognitionApiController(SpeechRecognitionService singleAsrService, ConcurrentSpeechRecognitionService batchAsrService) {
this.singleAsrService = singleAsrService;
this.batchAsrService = batchAsrService;
}
/**
* Point d'API pour la transcription d'un seul fichier audio.
* @param audioFile Le fichier audio à transcrire.
* @return Le résultat de la reconnaissance vocale.
*/
@PostMapping(value = "/transcribe-single", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<RecognitionResult> recognizeSingleAudio(
@RequestParam("file") @NotNull @Valid MultipartFile audioFile) {
if (audioFile.isEmpty()) {
throw new IllegalArgumentException("Le fichier audio ne doit pas être vide.");
}
if (!isSupportedAudioFormat(audioFile)) {
throw new IllegalArgumentException("Le format audio fourni n'est pas supporté.");
}
long startTime = System.currentTimeMillis();
try {
RecognitionResult result = singleAsrService.submitAudioForRecognition(audioFile)
.get(45, TimeUnit.SECONDS); // Attente maximale pour la transcription
result.setProcessingDurationMillis(System.currentTimeMillis() - startTime);
return ResponseEntity.ok(result);
} catch (java.util.concurrent.TimeoutException e) {
log.error("Délai de transcription expiré pour le fichier {}", audioFile.getOriginalFilename());
throw new ResponseStatusException(HttpStatus.REQUEST_TIMEOUT, "Le service de reconnaissance vocale n'a pas répondu à temps.");
} catch (Exception e) {
log.error("Échec de la transcription pour le fichier {}", audioFile.getOriginalFilename(), e);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Une erreur est survenue lors de la transcription.");
}
}
/**
* Point d'API pour la transcription de plusieurs fichiers audio en mode batch.
* @param audioFiles La liste des fichiers audio à transcrire.
* @return Une liste des résultats de reconnaissance vocale.
*/
@PostMapping(value = "/transcribe-batch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<List<RecognitionResult>> recognizeBatchAudio(
@RequestParam("files") @NotNull List<MultipartFile> audioFiles) {
if (audioFiles.isEmpty()) {
throw new IllegalArgumentException("La liste des fichiers audio ne doit pas être vide.");
}
if (audioFiles.size() > 50) { // Limite le nombre de fichiers par requête batch
throw new IllegalArgumentException("Le traitement par lots est limité à 50 fichiers par requête.");
}
List<CompletableFuture<RecognitionResult>> recognitionFutures =
batchAsrService.processMultipleAudioFiles(audioFiles);
List<RecognitionResult> results = recognitionFutures.stream()
.map(future -> {
long startTime = System.currentTimeMillis();
try {
RecognitionResult res = future.get(90, TimeUnit.SECONDS); // Délai global pour le batch
res.setProcessingDurationMillis(System.currentTimeMillis() - startTime);
return res;
} catch (Exception e) {
log.warn("Un fichier du lot a échoué la transcription : {}", e.getMessage());
// Retourne un résultat d'erreur pour ce fichier
return new RecognitionResult("", "failed", 0.0, System.currentTimeMillis() - startTime);
}
})
.collect(Collectors.toList());
return ResponseEntity.ok(results);
}
/**
* Vérifie si le format du fichier audio est supporté.
*/
private boolean isSupportedAudioFormat(MultipartFile file) {
String contentType = file.getContentType();
return contentType != null &&
(contentType.startsWith("audio/") ||
contentType.equals("application/octet-stream")); // Pour les formats génériques
}
}
- Conseils de déploiement et d'exploitation
Lors du déploiement en production, plusieurs aspects supplémentaires doivent être pris en compte :
- Conteneurisation Docker : Empaquetez le service d'inférence Python et l'application Spring Boot dans des conteneurs Docker séparés. Utilisez Docker Compose ou Kubernetes pour une gestion unifiée.
- Sondes de vitalité et de disponibilité : Implémentez des points de terminaison de santé pour les deux services (par exemple,
/actuator/healthpour Spring Boot, et un point dédié pour FastAPI) afin de garantir leur bon fonctionnement. - Centralisation des logs : Mettez en place un système de collecte et d'analyse des logs (comme ELK Stack ou Grafana Loki avec Promtail) pour faciliter le débogage et la surveillance.
- Scalabilité automatique : Configurez l'autoscaling horizontal basé sur la charge pour ajuster dynamiquement le nombre d'instances des services, notamment le service d'inférence Python qui peut être gourmand en ressources.
- Stratégie de mise en cache : Envisagez une couche de cache (par exemple, Redis) pour les transcriptions d'audio fréquemment demandées afin de réduire la charge sur le service ASR.
- Sécurité : Sécurisez les points d'API avec des mécanismes d'authentification et d'autorisation appropriés (OAuth2, JWT).