定期汇总转写的文本

This commit is contained in:
2026-04-04 17:12:44 +08:00
parent 28fc0d0e50
commit da6dab7dd1
6 changed files with 191 additions and 1 deletions

View File

@@ -2,22 +2,37 @@ package com.rj.scheduler;
import com.rj.config.AppConfig;
import com.rj.entity.AudioManagementSegments;
import com.rj.mapper.AudioManagementMapper;
import com.rj.mapper.AudioManagementSegmentsMapper;
import com.rj.service.IAudioManagementSegmentsService;
import com.rj.service.IAudioManagementStatisticsService;
import com.rj.service.MinIOService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 门店录音统计定时任务
@@ -41,6 +56,131 @@ public class AudioStatisticsScheduler {
@Autowired
private MinIOService minIOService;
@Autowired
private AudioManagementSegmentsMapper audioManagementSegmentsMapper;
@Autowired
private AudioManagementMapper audioManagementMapper;
private static final Set<String> TRANSCRIPT_TEXT_SUFFIXES = Set.of(".txt", ".text");
@Value("${app.audio.upload.yihangyi.txt-scan-dir:/home/lizh/java_env/AIDriverEEBackend/audio/yihangyi_txt}")
private String yihangyiTxtScanDir;
@Value("${app.audio.upload.yihangyi.txt-finish-dir:/home/lizh/java_env/AIDriverEEBackend/audio/yihangyi_txt_finish}")
private String yihangyiTxtFinishDir;
/**
* 每 2 分钟:扫描目录中的转写文本,按「文件名去扩展名 + .mp3」匹配 {@code audio_file_original_name}
* 写入 {@code audio_management_segments.recording_text},成功后移至完成目录并汇总回写 {@code audio_management}。
*/
@Scheduled(fixedRate = 120000)
public void syncYihangyiTranscriptTextFiles() {
if (!appConfig.getScheduler().isStart()) {
return;
}
if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) {
return;
}
Path scanDir = Paths.get(yihangyiTxtScanDir);
if (!Files.isDirectory(scanDir)) {
log.error("yihangyi 转写文本扫描目录不存在或不是目录: {}", scanDir.toAbsolutePath());
return;
}
List<Path> files;
try (Stream<Path> stream = Files.list(scanDir)) {
files = stream
.filter(Files::isRegularFile)
.filter(p -> {
String name = p.getFileName().toString();
int dot = name.lastIndexOf('.');
String suf = dot < 0 ? "" : name.substring(dot).toLowerCase(Locale.ROOT);
return TRANSCRIPT_TEXT_SUFFIXES.contains(suf);
})
.sorted(Comparator.comparing(p -> p.getFileName().toString()))
.collect(Collectors.toList());
} catch (IOException e) {
log.error("列出 yihangyi 转写文本目录失败: {}", scanDir.toAbsolutePath(), e);
return;
}
log.info("yihangyi 转写文本本次扫描到文件数: {}", files.size());
for (Path path : files) {
try {
syncOneTranscriptFile(path);
} catch (Exception e) {
log.error("处理 yihangyi 转写文本文件失败: {}", path.toAbsolutePath(), e);
}
}
}
private void syncOneTranscriptFile(Path path) throws IOException {
String stem = fileNameWithoutExtension(path.getFileName().toString());
String audioFileOriginalName = stem + ".mp3";
String content = readTranscriptTextFile(path);
log.info("执行 UPDATE audio_management_segments SET recording_text=? WHERE audio_file_original_name=? 参数长度={}, name={}",
content != null ? content.length() : 0, audioFileOriginalName);
int n = audioManagementSegmentsMapper.updateRecordingTextByAudioFileOriginalNameIgnoreTenant(
content, audioFileOriginalName);
if (n == 0) {
log.warn("未找到 audio_file_original_name={} 的分段记录,跳过: {}", audioFileOriginalName, path.getFileName());
return;
}
log.info("已更新 recording_textaudio_file_original_name={},影响行数={}", audioFileOriginalName, n);
String parentId = audioManagementSegmentsMapper.selectParentIdByAudioFileOriginalNameLimit1(audioFileOriginalName);
Path finishDir = Paths.get(yihangyiTxtFinishDir);
try {
Files.createDirectories(finishDir);
Path dest = finishDir.resolve(path.getFileName());
Files.move(path, dest, StandardCopyOption.REPLACE_EXISTING);
log.info("更新成功,已移动文件: {} -> {}", path.getFileName(), dest.toAbsolutePath());
} catch (Exception e) {
log.error("更新已成功但移动文件失败: {}", path.toAbsolutePath(), e);
return;
}
if (StringUtils.hasText(parentId)) {
try {
refreshAudioManagementFromSegments(parentId);
} catch (Exception e) {
log.error("移动成功后汇总回写 audio_management 失败 parent_id={}", parentId, e);
}
}
}
private static String fileNameWithoutExtension(String fileName) {
int dot = fileName.lastIndexOf('.');
return dot < 0 ? fileName : fileName.substring(0, dot);
}
private static String readTranscriptTextFile(Path path) throws IOException {
byte[] bytes = Files.readAllBytes(path);
return StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE)
.decode(ByteBuffer.wrap(bytes))
.toString();
}
private void refreshAudioManagementFromSegments(String parentId) {
List<AudioManagementSegments> rows = audioManagementSegmentsMapper.listRecordingPartsByParentId(parentId);
List<String> parts = new ArrayList<>();
BigDecimal durationTotal = BigDecimal.ZERO;
for (AudioManagementSegments row : rows) {
String rt = row.getRecordingText();
if (rt != null && !rt.isBlank()) {
parts.add(rt);
}
if (row.getDuration() != null) {
durationTotal = durationTotal.add(row.getDuration());
}
}
String recordingTextTotal = String.join("\n", parts);
log.info("汇总回写 audio_managementid={},子段数={}", parentId, rows.size());
int um = audioManagementMapper.updateRecordingTextAndDurationByIdIgnoreTenant(
recordingTextTotal, durationTotal, parentId);
log.info("已汇总回写 audio_management id={},子段数={},影响行数={}duration_total={}",
parentId, rows.size(), um, durationTotal);
}
/**
* 每 5 分钟:将仅有本地路径、尚未写入 MinIO URL 的录音分段补传到 Minio 并回写 audio_file_url。
*/
@@ -49,6 +189,9 @@ public class AudioStatisticsScheduler {
if (!appConfig.getScheduler().isStart()) {
return;
}
if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) {
return;
}
try {
List<AudioManagementSegments> pending = audioManagementSegmentsService.listSegmentsNeedingMinioUpload(50);
if (pending.isEmpty()) {