Mise en œuvre de Qwen3-ASR-0.6B en Production : Guide d'intégration Spring Boot avec optimisation des ressources

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.

  1. 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

  1. 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
}

  1. 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)
            );
    }
}

  1. 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;
        }
    }
}

  1. 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
    }
}

  1. 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/health pour 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).

Étiquettes: Qwen3-ASR Spring Boot FastAPI Reconnaissance Vocale Déploiement Entreprise

Publié le 14 juillet à 08h50