From 7b2691b98ff1bcd41ce4feabb2ebe40b2c58f87c Mon Sep 17 00:00:00 2001 From: cst61 Date: Sat, 2 May 2026 23:05:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8Apython=E4=BB=A3=E7=A0=81=E8=BD=AC?= =?UTF-8?q?=E4=B8=BAJava=EF=BC=8C=20=E6=B6=89=E5=8F=8A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=B8=BB=E8=A6=81=E6=98=AF=E9=9F=B3=E9=A2=91=E8=BD=AC=E6=96=87?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../rj/AISmartCard20251230Application.java | 3 + .../rj/config/AppSchedulingConfiguration.java | 28 +++ .../YihangyiVllmAsrEnablementListener.java | 23 ++ .../config/YihangyiVllmAsrExecutorMode.java | 11 + .../rj/config/YihangyiVllmAsrProperties.java | 42 ++++ .../mapper/AudioManagementSegmentsMapper.java | 8 + .../scheduler/AudioStatisticsScheduler.java | 2 + .../scheduler/YihangyiVllmAsrScheduler.java | 153 ++++++++++++ .../rj/service/YihangyiVllmAsrService.java | 221 ++++++++++++++++++ src/main/resources/application-audio.yml | 15 ++ src/main/resources/application.yml | 12 + 12 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/rj/config/AppSchedulingConfiguration.java create mode 100644 src/main/java/com/rj/config/YihangyiVllmAsrEnablementListener.java create mode 100644 src/main/java/com/rj/config/YihangyiVllmAsrExecutorMode.java create mode 100644 src/main/java/com/rj/config/YihangyiVllmAsrProperties.java create mode 100644 src/main/java/com/rj/scheduler/YihangyiVllmAsrScheduler.java create mode 100644 src/main/java/com/rj/service/YihangyiVllmAsrService.java diff --git a/pom.xml b/pom.xml index 88635b6..9e83459 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.cst AIDriverEEBackend - 1.26040419.6-SNAPSHOT + 1.260502.1-SNAPSHOT Langchain4j-rj Langchain4j-rj20250803 diff --git a/src/main/java/com/rj/AISmartCard20251230Application.java b/src/main/java/com/rj/AISmartCard20251230Application.java index 2ea56b5..2dff60f 100644 --- a/src/main/java/com/rj/AISmartCard20251230Application.java +++ b/src/main/java/com/rj/AISmartCard20251230Application.java @@ -1,8 +1,10 @@ package com.rj; +import com.rj.config.YihangyiVllmAsrProperties; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.scheduling.annotation.EnableScheduling; /** @@ -37,6 +39,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; @MapperScan("com.rj.mapper") @SpringBootApplication @EnableScheduling +@EnableConfigurationProperties(YihangyiVllmAsrProperties.class) public class AISmartCard20251230Application { public static void main(String[] args) { diff --git a/src/main/java/com/rj/config/AppSchedulingConfiguration.java b/src/main/java/com/rj/config/AppSchedulingConfiguration.java new file mode 100644 index 0000000..fd3a044 --- /dev/null +++ b/src/main/java/com/rj/config/AppSchedulingConfiguration.java @@ -0,0 +1,28 @@ +package com.rj.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +/** + * 显式扩大 {@code @Scheduled} 线程池,避免单个耗时任务(如长时间 HTTP)占满默认单线程后拖死全部定时任务。 + *

与 {@code spring.task.scheduling.pool.size} 配置互补;此处代码保证至少 8 个调度线程。 + */ +@Configuration +public class AppSchedulingConfiguration implements SchedulingConfigurer { + + private static final int SCHEDULER_POOL_SIZE = 8; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(SCHEDULER_POOL_SIZE); + scheduler.setThreadNamePrefix("app-scheduling-"); + scheduler.setWaitForTasksToCompleteOnShutdown(true); + scheduler.setAwaitTerminationSeconds(120); + scheduler.setRemoveOnCancelPolicy(true); + scheduler.initialize(); + taskRegistrar.setTaskScheduler(scheduler); + } +} diff --git a/src/main/java/com/rj/config/YihangyiVllmAsrEnablementListener.java b/src/main/java/com/rj/config/YihangyiVllmAsrEnablementListener.java new file mode 100644 index 0000000..b82f0bb --- /dev/null +++ b/src/main/java/com/rj/config/YihangyiVllmAsrEnablementListener.java @@ -0,0 +1,23 @@ +package com.rj.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 说明 {@link com.rj.scheduler.YihangyiVllmAsrScheduler} 的加载条件,避免与根节点 {@code asr.*} 混淆。 + */ +@Slf4j +@Component +@ConditionalOnProperty(name = "app.audio.upload.yihangyi.asr.enabled", havingValue = "false", matchIfMissing = true) +public class YihangyiVllmAsrEnablementListener { + + @EventListener(ApplicationReadyEvent.class) + public void onReady() { + log.info( + "YihangyiVllmAsrScheduler 未启用:需设置 app.audio.upload.yihangyi.asr.enabled=true(当前为 false 或未配置)。" + + "根配置 asr.enabled 用于其它 ASR 能力,不会加载本调度类。"); + } +} diff --git a/src/main/java/com/rj/config/YihangyiVllmAsrExecutorMode.java b/src/main/java/com/rj/config/YihangyiVllmAsrExecutorMode.java new file mode 100644 index 0000000..e55f959 --- /dev/null +++ b/src/main/java/com/rj/config/YihangyiVllmAsrExecutorMode.java @@ -0,0 +1,11 @@ +package com.rj.config; + +/** + * yihangyi 目录轮询 ASR:单线程顺序处理,或线程池并行处理本批文件。 + */ +public enum YihangyiVllmAsrExecutorMode { + /** 同批文件逐个处理(与 Python 脚本一致)。 */ + SINGLE, + /** 同批文件提交到固定大小线程池并行处理。 */ + POOL +} diff --git a/src/main/java/com/rj/config/YihangyiVllmAsrProperties.java b/src/main/java/com/rj/config/YihangyiVllmAsrProperties.java new file mode 100644 index 0000000..43211a8 --- /dev/null +++ b/src/main/java/com/rj/config/YihangyiVllmAsrProperties.java @@ -0,0 +1,42 @@ +package com.rj.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * vLLM OpenAI 兼容 ASR 轮询配置(对应 Python {@code audio_toText_qwen3_asr_17b_vllm_interface.py})。 + */ +@Data +@ConfigurationProperties(prefix = "app.audio.upload.yihangyi.asr") +public class YihangyiVllmAsrProperties { + + /** 是否启用定时轮询(默认关闭,避免本地/Windows 误扫生产目录)。 */ + private boolean enabled = false; + + private String watchDir = "/home/lizh/java_env/AIDriverEEBackend/audio/yihangyi"; + + private String doneDir = "/home/lizh/java_env/AIDriverEEBackend/audio/yihangyi_finish"; + + /** 转写 txt 输出目录,与 {@link com.rj.scheduler.AudioStatisticsScheduler} 扫描目录一致。 */ + private String txtDir = "/home/lizh/java_env/AIDriverEEBackend/audio/yihangyi_txt"; + + /** OpenAI 兼容 base-url,须含 /v1,例如 {@code http://host:17001/v1} */ + private String baseUrl = "http://101.35.52.237:17001/v1"; + + private String apiKey = "EMPTY"; + + private String model = "Qwen3-ASR-1.7B"; + + /** 与 Python POLL_INTERVAL_SEC 一致(毫秒)。 */ + private long pollIntervalMs = 50000L; + + private int maxBatch = 5; + + private YihangyiVllmAsrExecutorMode executorMode = YihangyiVllmAsrExecutorMode.SINGLE; + + /** executorMode=POOL 时的工作线程数。 */ + private int poolSize = 4; + + /** 是否在 Windows 上跳过执行(与其它 yihangyi 定时任务一致)。 */ + private boolean skipOnWindows = true; +} diff --git a/src/main/java/com/rj/mapper/AudioManagementSegmentsMapper.java b/src/main/java/com/rj/mapper/AudioManagementSegmentsMapper.java index 2649ad3..aa7f4fe 100644 --- a/src/main/java/com/rj/mapper/AudioManagementSegmentsMapper.java +++ b/src/main/java/com/rj/mapper/AudioManagementSegmentsMapper.java @@ -45,6 +45,14 @@ public interface AudioManagementSegmentsMapper extends BaseMapper不使用 Spring {@code @Scheduled},避免与全局定时线程池争抢; + * 在独立单线程上按固定间隔执行(与 Python 常驻循环等价),批内仍可选 POOL 并行转写。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(name = "app.audio.upload.yihangyi.asr.enabled", havingValue = "true") +public class YihangyiVllmAsrScheduler { + + private final YihangyiVllmAsrProperties properties; + private final YihangyiVllmAsrService yihangyiVllmAsrService; + private final AppConfig appConfig; + + private ExecutorService workerPool; + private ScheduledExecutorService pollExecutor; + + @PostConstruct + void start() { + if (properties.getExecutorMode() == YihangyiVllmAsrExecutorMode.POOL) { + int n = Math.max(1, properties.getPoolSize()); + workerPool = Executors.newFixedThreadPool(n, r -> { + Thread t = new Thread(r, "yihangyi-asr-worker"); + t.setDaemon(true); + return t; + }); + log.info("yihangyi ASR 批内使用线程池,线程数={}", n); + } else { + log.info("yihangyi ASR 批内单线程顺序处理"); + } + long intervalMs = Math.max(1000L *60*3, properties.getPollIntervalMs()); + pollExecutor = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "yihangyi-asr-poll"); + t.setDaemon(true); + return t; + }); + pollExecutor.scheduleWithFixedDelay(this::pollSafe, 0, intervalMs, TimeUnit.MILLISECONDS); + log.info("yihangyi ASR 独立轮询线程已启动,fixedDelay={}ms(不占 Spring @Scheduled 线程池)", intervalMs); + } + + @PreDestroy + void shutdown() { + if (pollExecutor != null) { + pollExecutor.shutdown(); + try { + if (!pollExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + pollExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + pollExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + if (workerPool != null) { + workerPool.shutdown(); + try { + if (!workerPool.awaitTermination(60, TimeUnit.SECONDS)) { + workerPool.shutdownNow(); + } + } catch (InterruptedException e) { + workerPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + private void pollSafe() { + try { + log.info("yihangyi ASR 音频-----》文本, 轮询开始"); + pollAndTranscribe(); + } catch (Throwable t) { + log.error("yihangyi ASR 轮询未捕获异常", t); + } + } + + void pollAndTranscribe() { + if (!appConfig.getScheduler().isStart()) { + log.info("yihangyi ASR 调度跳过:app.scheduler.start=false"); + return; + } + if (properties.isSkipOnWindows() + && System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) { + log.info("yihangyi ASR 调度跳过:当前为 Windows 且 app.audio.upload.yihangyi.asr.skip-on-windows=true"); + return; + } + Path watchDir = Path.of(properties.getWatchDir()); + int maxBatch = Math.max(1, properties.getMaxBatch()); + long t0 = System.nanoTime(); + log.info( + "yihangyi ASR 调度开始:watchDir={} executorMode={} maxBatch={} pollIntervalMs={}", + watchDir.toAbsolutePath(), + properties.getExecutorMode(), + maxBatch, + properties.getPollIntervalMs()); + + if (!java.nio.file.Files.isDirectory(watchDir)) { + log.warn("yihangyi ASR 调度结束(异常):监视目录不存在或不是文件夹: {}", watchDir.toAbsolutePath()); + return; + } + int pendingTotal = yihangyiVllmAsrService.countPendingAudio(watchDir); + log.info("yihangyi ASR 待转写音频文件总数: {}", pendingTotal); + + List pending = yihangyiVllmAsrService.listNewestPendingAudio(watchDir, maxBatch); + if (pending.isEmpty()) { + log.info( + "yihangyi ASR 调度结束:本批无待处理文件,待转写总数={},耗时={}ms", + pendingTotal, + (System.nanoTime() - t0) / 1_000_000); + return; + } + + log.info("yihangyi ASR 本批将处理 {} 个文件: {}", pending.size(), + pending.stream().map(p -> p.getFileName().toString()).toList()); + + if (properties.getExecutorMode() == YihangyiVllmAsrExecutorMode.POOL && workerPool != null) { + List> futures = pending.stream() + .map(p -> CompletableFuture.runAsync(() -> yihangyiVllmAsrService.processOneFile(p), workerPool)) + .toList(); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + } else { + for (Path p : pending) { + yihangyiVllmAsrService.processOneFile(p); + } + } + log.info( + "yihangyi ASR 调度结束:本批已处理 {} 个文件,待转写总数(处理前统计)={},耗时={}ms", + pending.size(), + pendingTotal, + (System.nanoTime() - t0) / 1_000_000); + } +} diff --git a/src/main/java/com/rj/service/YihangyiVllmAsrService.java b/src/main/java/com/rj/service/YihangyiVllmAsrService.java new file mode 100644 index 0000000..a1f80c4 --- /dev/null +++ b/src/main/java/com/rj/service/YihangyiVllmAsrService.java @@ -0,0 +1,221 @@ +package com.rj.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rj.config.YihangyiVllmAsrProperties; +import com.rj.mapper.AudioManagementSegmentsMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Stream; + +/** + * 监视目录 → 重命名为 *processing* → 调用 OpenAI 兼容 /v1/audio/transcriptions → 写 txt → 移至完成目录 → 更新 {@code audio_file_path}。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class YihangyiVllmAsrService { + + private static final Set AUDIO_EXTENSIONS = Set.of( + ".mp3", ".wav", ".m4a", ".flac", ".ogg", ".wma", ".aac", ".webm", ".opus" + ); + + private final YihangyiVllmAsrProperties properties; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + private final AudioManagementSegmentsMapper audioManagementSegmentsMapper; + + public int countPendingAudio(Path watchDir) { + if (!Files.isDirectory(watchDir)) { + return 0; + } + try (Stream stream = Files.list(watchDir)) { + return (int) stream.filter(Files::isRegularFile).filter(YihangyiVllmAsrService::isPendingAudioFile).count(); + } catch (IOException e) { + log.warn("统计待转写文件失败: {}", watchDir.toAbsolutePath(), e); + return 0; + } + } + + /** + * 按修改时间从新到旧,最多 {@code limit} 条。 + */ + public List listNewestPendingAudio(Path watchDir, int limit) { + if (!Files.isDirectory(watchDir) || limit <= 0) { + return List.of(); + } + List entries = new ArrayList<>(); + try (Stream stream = Files.list(watchDir)) { + for (Path p : stream.filter(Files::isRegularFile).toList()) { + if (!isPendingAudioFile(p)) { + continue; + } + try { + long mtime = Files.getLastModifiedTime(p).toMillis(); + entries.add(new PathWithMtime(p, mtime)); + } catch (IOException ignored) { + // skip + } + } + } catch (IOException e) { + log.warn("列出待转写文件失败: {}", watchDir.toAbsolutePath(), e); + return List.of(); + } + entries.sort(Comparator.comparingLong(PathWithMtime::mtime).reversed()); + return entries.stream().limit(limit).map(PathWithMtime::path).toList(); + } + + /** + * 处理单个原始路径(调用前须已确认为待处理音频)。失败时打日志;转写失败则文件保持 *processing* 名(与 Python 一致)。 + */ + public void processOneFile(Path src) { + Path watchDir = src.getParent(); + if (watchDir == null) { + log.warn("[skip] 无父目录: {}", src); + return; + } + Path dst = processingPath(src); + try { + Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + log.warn("[skip] 重命名失败 {} -> {}: {}", src, dst, e.getMessage()); + return; + } + long t0 = System.nanoTime(); + try { + String text = transcribe(dst); + double elapsedSec = (System.nanoTime() - t0) / 1_000_000_000.0; + log.info("ASR 完成 file={} 耗时={}s 字数={}", dst.getFileName(), String.format(Locale.ROOT, "%.3f", elapsedSec), + text != null ? text.length() : 0); + if (log.isDebugEnabled()) { + log.debug("ASR 文本: {}", text); + } + + Path txtDir = Path.of(properties.getTxtDir()); + try { + writeTranscriptionTxt(txtDir, src, text != null ? text : ""); + } catch (IOException e) { + log.error("[error] 转写成功但写入文本失败: {}", txtDir.resolve(txtFileName(src)), e); + } + + Path doneDir = Path.of(properties.getDoneDir()); + try { + Files.createDirectories(doneDir); + Path finalPath = doneDir.resolve(src.getFileName()); + Files.move(dst, finalPath, StandardCopyOption.REPLACE_EXISTING); + String abs = finalPath.toAbsolutePath().normalize().toString(); + int n = audioManagementSegmentsMapper.updateAudioFilePathByAudioFileOriginalNameIgnoreTenant( + abs, src.getFileName().toString()); + if (n == 0) { + log.warn("[warn] 未找到 audio_file_original_name={} 的记录,audio_file_path 未更新", + src.getFileName()); + } else { + log.info("已更新 audio_file_path({} 行): {} -> {}", n, src.getFileName(), abs); + } + } catch (IOException e) { + log.error("[error] 转写成功但移动失败 {} -> {}", dst, doneDir.resolve(src.getFileName()), e); + } + } catch (Exception e) { + log.error("[error] 转写失败 {}", dst, e); + } + } + + private record PathWithMtime(Path path, long mtime) {} + + public static boolean isPendingAudioFile(Path path) { + if (!Files.isRegularFile(path)) { + return false; + } + String name = path.getFileName().toString(); + String lower = name.toLowerCase(Locale.ROOT); + int dot = lower.lastIndexOf('.'); + String suf = dot < 0 ? "" : lower.substring(dot); + if (!AUDIO_EXTENSIONS.contains(suf)) { + return false; + } + return !lower.contains("processing"); + } + + public static Path processingPath(Path path) { + String stem = fileStem(path.getFileName().toString()); + String extWithDot = fileExtensionWithDot(path.getFileName().toString()); + return path.resolveSibling(stem + "processing" + extWithDot); + } + + private static String fileStem(String fileName) { + int dot = fileName.lastIndexOf('.'); + return dot < 0 ? fileName : fileName.substring(0, dot); + } + + private static String fileExtensionWithDot(String fileName) { + int dot = fileName.lastIndexOf('.'); + return dot < 0 ? "" : fileName.substring(dot); + } + + private static String txtFileName(Path originalAudio) { + return fileStem(originalAudio.getFileName().toString()) + ".txt"; + } + + private void writeTranscriptionTxt(Path txtDir, Path originalAudio, String text) throws IOException { + Files.createDirectories(txtDir); + Path out = txtDir.resolve(txtFileName(originalAudio)); + Files.writeString(out, text, StandardCharsets.UTF_8); + } + + private String transcribe(Path audioPath) throws IOException { + String base = properties.getBaseUrl().trim(); + if (base.endsWith("/")) { + base = base.substring(0, base.length() - 1); + } + String url = base + "/audio/transcriptions"; + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("model", properties.getModel()); + body.add("file", new FileSystemResource(audioPath.toFile())); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + headers.setBearerAuth(properties.getApiKey() != null ? properties.getApiKey() : ""); + + HttpEntity> request = new HttpEntity<>(body, headers); + try { + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { + throw new IOException("ASR HTTP 非成功: " + response.getStatusCode() + " body=" + response.getBody()); + } + return parseTranscriptionText(response.getBody()); + } catch (RestClientException e) { + throw new IOException("ASR 请求失败: " + e.getMessage(), e); + } + } + + private String parseTranscriptionText(String json) throws IOException { + JsonNode root = objectMapper.readTree(json); + JsonNode text = root.get("text"); + if (text != null && !text.isNull()) { + return text.asText(""); + } + throw new IOException("响应 JSON 缺少 text 字段: " + json); + } +} diff --git a/src/main/resources/application-audio.yml b/src/main/resources/application-audio.yml index b878b0c..0459e32 100644 --- a/src/main/resources/application-audio.yml +++ b/src/main/resources/application-audio.yml @@ -10,6 +10,21 @@ app: # 外部 ASR 写入的转写文本目录(与 AudioStatisticsScheduler.syncYihangyiTranscriptTextFiles 一致) txt-scan-dir: /home/lizh/java_env/AIDriverEEBackend/audio/yihangyi_txt txt-finish-dir: /home/lizh/java_env/AIDriverEEBackend/audio/yihangyi_txt_finish + # 内置 Java 轮询 ASR(对应 Python audio_toText_qwen3_asr_17b_vllm_interface.py);默认关闭 + asr: + enabled: false + watch-dir: /home/lizh/java_env/AIDriverEEBackend/audio/yihangyi + done-dir: /home/lizh/java_env/AIDriverEEBackend/audio/yihangyi_finish + txt-dir: /home/lizh/java_env/AIDriverEEBackend/audio/yihangyi_txt + base-url: http://101.35.52.237:17001/v1 + api-key: ${OPENAI_API_KEY:EMPTY} + model: Qwen3-ASR-1.7B + poll-interval-ms: 5000 + max-batch: 5 + # SINGLE:每批内单线程顺序;POOL:固定线程池并行处理本批 + executor-mode: SINGLE + pool-size: 4 + skip-on-windows: true # 音频文件访问URL前缀 access: url: /api/audio/ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e0cb018..1024bfd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,6 +5,12 @@ app: timezone: Asia/Shanghai scheduler: start: true + # yihangyi 目录轮询 ASR(YihangyiVllmAsrScheduler):仅当 asr.enabled=true 时才会创建 Bean 并输出日志;与下文根节点 asr.* 不是同一套配置 + audio: + upload: + yihangyi: + asr: + enabled: true # DashScope API配置 dashscope: api: @@ -86,6 +92,12 @@ spring: autoconfigure: exclude: - dev.langchain4j.community.store.embedding.redis.spring.RedisEmbeddingStoreAutoConfiguration + # 默认定时任务线程池为 1:长时间阻塞的 @Scheduled(如 ASR HTTP)会卡住其余全部调度任务 + task: + scheduling: + pool: + size: 8 + thread-name-prefix: app-scheduling- data: redis: port: 6389