把python代码转为Java, 涉及功能主要是音频转文本
This commit is contained in:
221
src/main/java/com/rj/service/YihangyiVllmAsrService.java
Normal file
221
src/main/java/com/rj/service/YihangyiVllmAsrService.java
Normal file
@@ -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<String> 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<Path> 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<Path> listNewestPendingAudio(Path watchDir, int limit) {
|
||||
if (!Files.isDirectory(watchDir) || limit <= 0) {
|
||||
return List.of();
|
||||
}
|
||||
List<PathWithMtime> entries = new ArrayList<>();
|
||||
try (Stream<Path> 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<String, Object> 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<MultiValueMap<String, Object>> request = new HttpEntity<>(body, headers);
|
||||
try {
|
||||
ResponseEntity<String> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user