Splice short lines into 30-second windows before transcribing
Whisper always encodes a full 30-second window, whatever the clip length, and the average line here is 4.4 seconds. Fed one at a time, 19.5 hours of speech costs what 135 hours of continuous audio would. Lines are now concatenated with a second of silence between them, filling a window with about five, and the result is cut back apart using the timestamps from whisper's JSON output. A segment goes to the line it overlaps most, so a line split across several segments is reassembled and a segment straying into the silence still lands correctly. Measured on 48 real lines, same set through both paths: speech (39) similarity 0.92, no text landed under the wrong file non-speech (9) similarity 0.54 — both modes invent music annotations Splicing turns out to be slightly *more* accurate on speech, because the model sees context: "Jeden raz czułem" becomes "Niejeden raz czułem", "muszę to pościć" becomes "puścić", "SOO?" becomes "Co?". On the whole collection this projects to 5.8 h → 3.5 h, a 1.7× gain rather than the 5× I claimed earlier from counting encoder windows alone. Encoding a window and decoding a line cost about the same, 0.65 s each, and decoding is per line no matter how the audio is packed — so only half the work can be folded away. Window size, gap and batch size are all settable, and catalog.whisper.concat=false restores the file-at-a-time path, which is how the two were compared. Output moved from -otxt to -oj because only the JSON carries the timestamps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a773d791a7
commit
b99f239553
@@ -3,8 +3,15 @@ package pl.genschu.rexcatalog.transcribe;
|
||||
import pl.genschu.rexcatalog.db.Database;
|
||||
import pl.genschu.rexcatalog.media.MediaIndex;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -12,8 +19,11 @@ import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
@@ -24,11 +34,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
* wydania ani z faktami detektorów: to hipoteza maszyny o tym, co słychać, a nie
|
||||
* zapis z nośnika.
|
||||
*
|
||||
* <p>Pliki idą <b>wsadami</b>, a nie po jednym: każde uruchomienie whisper.cpp
|
||||
* wczytuje model od nowa, co przy dużym modelu trwa dłużej niż rozpoznanie
|
||||
* kilkusekundowej kwestii. Przy siedemnastu tysiącach nagrań samo wczytywanie
|
||||
* zdominowałoby całą pracę. Wsad musi mieć jeden język, bo {@code -l} dotyczy
|
||||
* całego wywołania.
|
||||
* <p>Kwestie są <b>sklejane w okna</b>, a nie podawane po jednej. Whisper koduje
|
||||
* zawsze pełne trzydzieści sekund, niezależnie od tego, czy nagranie trwa dwie
|
||||
* sekundy, czy dwadzieścia — a w tej kolekcji średnia kwestia ma 4,4 s. Podawane
|
||||
* pojedynczo, dziewiętnaście godzin mowy kosztowałoby tyle, co sto trzydzieści
|
||||
* pięć godzin ciągłego nagrania. Sklejone z odstępami ciszy mieszczą się po kilka
|
||||
* w jednym oknie, a wynik rozcinamy z powrotem po znacznikach czasu.
|
||||
*
|
||||
* <p>Okno musi mieć jeden język, bo {@code -l} dotyczy całego wywołania.
|
||||
*
|
||||
* <p>Praca idzie w tle i da się ją przerwać w dowolnym momencie —
|
||||
* to, co już policzone, zostaje w bazie. Dzięki temu można puścić kolekcję na noc
|
||||
@@ -40,10 +53,30 @@ public final class Transcriber {
|
||||
private static final int WHISPER_RATE = 16000;
|
||||
|
||||
/**
|
||||
* Ile plików na jedno uruchomienie. Większy wsad lepiej amortyzuje wczytanie
|
||||
* modelu, ale przerwanie działa dopiero między wsadami.
|
||||
* Ile okien na jedno uruchomienie whisper.cpp. Każde uruchomienie wczytuje
|
||||
* model od nowa, więc kilka okien naraz amortyzuje ten koszt; przerwanie
|
||||
* działa dopiero między wsadami.
|
||||
*/
|
||||
private static final int BATCH = Integer.getInteger("catalog.whisper.batch", 16);
|
||||
private static final int BATCH = Integer.getInteger("catalog.whisper.batch", 8);
|
||||
|
||||
/**
|
||||
* Ile dźwięku upychamy w jedno okno. Whisper i tak liczy trzydzieści sekund,
|
||||
* ale zostawiamy zapas, żeby ostatnia kwestia nie wypadła poza okno.
|
||||
*/
|
||||
private static final int WINDOW_MS = Integer.getInteger("catalog.whisper.window", 27_000);
|
||||
|
||||
/**
|
||||
* Cisza między sklejonymi kwestiami. Musi wystarczyć, żeby model potraktował
|
||||
* je jako osobne wypowiedzi — przy zbyt krótkiej przerwie zlewa dwie kwestie
|
||||
* w jeden segment i tekst trafia pod niewłaściwy plik.
|
||||
*/
|
||||
private static final int GAP_MS = Integer.getInteger("catalog.whisper.gap", 1000);
|
||||
|
||||
/** Wyłącznik sklejania — do porównania wyników z trybem plik po pliku. */
|
||||
private static final boolean CONCAT =
|
||||
!"false".equals(System.getProperty("catalog.whisper.concat"));
|
||||
|
||||
private static final int BYTES_PER_MS = WHISPER_RATE * 2 / 1000;
|
||||
|
||||
/**
|
||||
* Wątki dla whisper.cpp. Zostawiamy jego własną wartość domyślną: na laptopie
|
||||
@@ -244,17 +277,14 @@ public final class Transcriber {
|
||||
|
||||
try {
|
||||
workDir = Files.createTempDirectory("rex-transkrypcja");
|
||||
for (List<Job> batch : batches(jobs)) {
|
||||
if (cancelled.get()) {
|
||||
break;
|
||||
}
|
||||
progress = new Progress("pracuje", jobs.size(), done, failed,
|
||||
batch.get(0).name(), System.currentTimeMillis() - started,
|
||||
progress.note());
|
||||
List<Window> pending = new ArrayList<>();
|
||||
|
||||
List<Path> inputs = new ArrayList<>();
|
||||
List<Job> ready = new ArrayList<>();
|
||||
for (Job job : batch) {
|
||||
for (List<Job> group : byLanguage(jobs)) {
|
||||
for (Job job : group) {
|
||||
if (cancelled.get()) {
|
||||
break;
|
||||
}
|
||||
byte[] pcm;
|
||||
try {
|
||||
byte[] raw;
|
||||
synchronized (lock) {
|
||||
@@ -265,41 +295,39 @@ public final class Transcriber {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
inputs.add(convert(workDir, raw, ready.size()));
|
||||
ready.add(job);
|
||||
pcm = toPcm(raw);
|
||||
} catch (Exception e) {
|
||||
System.err.printf(" ! %s: %s%n", job.name(), e.getMessage());
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Window last = pending.isEmpty() ? null : pending.get(pending.size() - 1);
|
||||
if (last == null || !last.accepts(job, pcm)) {
|
||||
last = new Window(job.lang());
|
||||
pending.add(last);
|
||||
}
|
||||
last.add(job, pcm);
|
||||
|
||||
if (pending.size() >= BATCH) {
|
||||
int[] result = flush(tool, workDir, pending, jobs.size(), done, failed,
|
||||
started);
|
||||
done = result[0];
|
||||
failed = result[1];
|
||||
pending.clear();
|
||||
}
|
||||
}
|
||||
if (ready.isEmpty()) {
|
||||
continue;
|
||||
// język się zmienia, więc niedomknięte okno musi pójść teraz
|
||||
if (!pending.isEmpty()) {
|
||||
int[] result = flush(tool, workDir, pending, jobs.size(), done, failed,
|
||||
started);
|
||||
done = result[0];
|
||||
failed = result[1];
|
||||
pending.clear();
|
||||
}
|
||||
|
||||
try {
|
||||
transcribe(tool, inputs, ready.get(0).lang());
|
||||
} catch (Exception e) {
|
||||
System.err.printf(" ! wsad %d plików: %s%n", ready.size(), e.getMessage());
|
||||
failed += ready.size();
|
||||
clean(inputs);
|
||||
continue;
|
||||
if (cancelled.get()) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < ready.size(); i++) {
|
||||
try {
|
||||
String text = withoutHallucinations(readResult(inputs.get(i)));
|
||||
synchronized (lock) {
|
||||
// pusty wynik też zapisujemy: cisza albo sam efekt dźwiękowy,
|
||||
// inaczej ten plik wracałby przy każdym kolejnym przebiegu
|
||||
store(ready.get(i), text == null ? "" : text, tool);
|
||||
}
|
||||
done++;
|
||||
} catch (Exception e) {
|
||||
System.err.printf(" ! %s: %s%n", ready.get(i).name(), e.getMessage());
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
clean(inputs);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
progress = new Progress("błąd", jobs.size(), done, failed, null,
|
||||
@@ -313,26 +341,236 @@ public final class Transcriber {
|
||||
done, failed, null, System.currentTimeMillis() - started, progress.note());
|
||||
}
|
||||
|
||||
/** Dzieli pracę na wsady jednego języka — {@code -l} obowiązuje całe wywołanie. */
|
||||
static List<List<Job>> batches(List<Job> jobs) {
|
||||
java.util.Map<String, List<Job>> byLang = new java.util.LinkedHashMap<>();
|
||||
for (Job job : jobs) {
|
||||
byLang.computeIfAbsent(job.lang(), k -> new ArrayList<>()).add(job);
|
||||
}
|
||||
List<List<Job>> out = new ArrayList<>();
|
||||
for (List<Job> group : byLang.values()) {
|
||||
for (int i = 0; i < group.size(); i += BATCH) {
|
||||
out.add(group.subList(i, Math.min(i + BATCH, group.size())));
|
||||
/** Przetwarza zebrane okna i zwraca zaktualizowane liczniki {@code {zrobione, błędy}}. */
|
||||
private int[] flush(Tool tool, Path workDir, List<Window> windows, int total, int done,
|
||||
int failed, long started) {
|
||||
progress = new Progress("pracuje", total, done, failed,
|
||||
windows.get(0).first(), System.currentTimeMillis() - started, progress.note());
|
||||
|
||||
List<Path> inputs = new ArrayList<>();
|
||||
for (int i = 0; i < windows.size(); i++) {
|
||||
try {
|
||||
inputs.add(windows.get(i).write(workDir, i));
|
||||
} catch (Exception e) {
|
||||
System.err.printf(" ! okno %d: %s%n", i, e.getMessage());
|
||||
failed += windows.get(i).jobs().size();
|
||||
return new int[]{done, failed};
|
||||
}
|
||||
}
|
||||
return out;
|
||||
|
||||
try {
|
||||
runWhisper(tool, inputs, windows.get(0).lang());
|
||||
} catch (Exception e) {
|
||||
System.err.printf(" ! whisper: %s%n", e.getMessage());
|
||||
for (Window window : windows) {
|
||||
failed += window.jobs().size();
|
||||
}
|
||||
clean(inputs);
|
||||
return new int[]{done, failed};
|
||||
}
|
||||
|
||||
for (int i = 0; i < windows.size(); i++) {
|
||||
Window window = windows.get(i);
|
||||
Map<String, String> texts;
|
||||
try {
|
||||
texts = window.split(readSegments(inputs.get(i)));
|
||||
} catch (Exception e) {
|
||||
System.err.printf(" ! okno %d: %s%n", i, e.getMessage());
|
||||
failed += window.jobs().size();
|
||||
continue;
|
||||
}
|
||||
for (Job job : window.jobs()) {
|
||||
try {
|
||||
synchronized (lock) {
|
||||
store(job, withoutHallucinations(texts.getOrDefault(job.sha1(), "")),
|
||||
tool);
|
||||
}
|
||||
done++;
|
||||
} catch (Exception e) {
|
||||
System.err.printf(" ! %s: %s%n", job.name(), e.getMessage());
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
clean(inputs);
|
||||
return new int[]{done, failed};
|
||||
}
|
||||
|
||||
private static List<List<Job>> byLanguage(List<Job> jobs) {
|
||||
Map<String, List<Job>> grouped = new LinkedHashMap<>();
|
||||
for (Job job : jobs) {
|
||||
grouped.computeIfAbsent(job.lang(), k -> new ArrayList<>()).add(job);
|
||||
}
|
||||
return new ArrayList<>(grouped.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Okno: kilka kwestii sklejonych ciszą w jeden strumień, razem z zapisem tego,
|
||||
* gdzie która się zaczyna. Bez tych pozycji nie dałoby się przypisać wyniku
|
||||
* z powrotem do pliku.
|
||||
*/
|
||||
private static final class Window {
|
||||
private final String lang;
|
||||
private final List<Job> jobs = new ArrayList<>();
|
||||
private final List<int[]> spans = new ArrayList<>();
|
||||
private final ByteArrayOutputStream pcm = new ByteArrayOutputStream();
|
||||
|
||||
Window(String lang) {
|
||||
this.lang = lang;
|
||||
}
|
||||
|
||||
String lang() {
|
||||
return lang;
|
||||
}
|
||||
|
||||
List<Job> jobs() {
|
||||
return jobs;
|
||||
}
|
||||
|
||||
String first() {
|
||||
return jobs.isEmpty() ? null : jobs.get(0).name();
|
||||
}
|
||||
|
||||
/** Puste okno przyjmie wszystko — także kwestię dłuższą niż samo okno. */
|
||||
boolean accepts(Job job, byte[] audio) {
|
||||
if (!lang.equals(job.lang())) {
|
||||
return false;
|
||||
}
|
||||
if (jobs.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
// z wyłączonym sklejaniem każde okno niesie jedną kwestię; reszta
|
||||
// ścieżki zostaje ta sama, więc oba tryby da się porównać wprost
|
||||
return CONCAT && lengthMs() + GAP_MS + msOf(audio) <= WINDOW_MS;
|
||||
}
|
||||
|
||||
void add(Job job, byte[] audio) {
|
||||
try {
|
||||
if (!jobs.isEmpty()) {
|
||||
pcm.write(new byte[GAP_MS * BYTES_PER_MS]);
|
||||
}
|
||||
int start = lengthMs();
|
||||
pcm.write(audio);
|
||||
spans.add(new int[]{start, lengthMs()});
|
||||
jobs.add(job);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private int lengthMs() {
|
||||
return pcm.size() / BYTES_PER_MS;
|
||||
}
|
||||
|
||||
private static int msOf(byte[] audio) {
|
||||
return audio.length / BYTES_PER_MS;
|
||||
}
|
||||
|
||||
Path write(Path workDir, int index) throws IOException {
|
||||
Path wav = workDir.resolve("okno" + index + ".wav");
|
||||
byte[] data = pcm.toByteArray();
|
||||
try (OutputStream out = Files.newOutputStream(wav)) {
|
||||
out.write(wavHeader(data.length));
|
||||
out.write(data);
|
||||
}
|
||||
return wav;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rozdziela segmenty whispera z powrotem na kwestie. Segment trafia tam,
|
||||
* gdzie ma największe pokrycie — jedna kwestia bywa rozbita na kilka
|
||||
* segmentów, a segment potrafi zahaczyć o ciszę.
|
||||
*/
|
||||
Map<String, String> split(List<int[]> segments, List<String> texts) {
|
||||
Map<String, StringBuilder> out = new LinkedHashMap<>();
|
||||
for (int i = 0; i < segments.size(); i++) {
|
||||
int best = -1;
|
||||
int bestOverlap = 0;
|
||||
for (int j = 0; j < spans.size(); j++) {
|
||||
int overlap = Math.min(segments.get(i)[1], spans.get(j)[1])
|
||||
- Math.max(segments.get(i)[0], spans.get(j)[0]);
|
||||
if (overlap > bestOverlap) {
|
||||
bestOverlap = overlap;
|
||||
best = j;
|
||||
}
|
||||
}
|
||||
if (best < 0) {
|
||||
continue;
|
||||
}
|
||||
out.computeIfAbsent(jobs.get(best).sha1(), k -> new StringBuilder())
|
||||
.append(texts.get(i).trim()).append(' ');
|
||||
}
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
out.forEach((sha1, text) -> result.put(sha1, text.toString().trim()));
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, String> split(Segments segments) {
|
||||
return split(segments.ranges(), segments.texts());
|
||||
}
|
||||
}
|
||||
|
||||
/** Nagłówek RIFF dla 16 kHz mono, 16 bitów — dokładnie tego oczekuje whisper.cpp. */
|
||||
private static byte[] wavHeader(int dataLength) {
|
||||
ByteBuffer header = ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN);
|
||||
header.put("RIFF".getBytes(StandardCharsets.US_ASCII));
|
||||
header.putInt(36 + dataLength);
|
||||
header.put("WAVEfmt ".getBytes(StandardCharsets.US_ASCII));
|
||||
header.putInt(16);
|
||||
header.putShort((short) 1);
|
||||
header.putShort((short) 1);
|
||||
header.putInt(WHISPER_RATE);
|
||||
header.putInt(WHISPER_RATE * 2);
|
||||
header.putShort((short) 2);
|
||||
header.putShort((short) 16);
|
||||
header.put("data".getBytes(StandardCharsets.US_ASCII));
|
||||
header.putInt(dataLength);
|
||||
return header.array();
|
||||
}
|
||||
|
||||
/** Segmenty odczytane z wyniku whispera: zakresy czasu i odpowiadający im tekst. */
|
||||
record Segments(List<int[]> ranges, List<String> texts) {
|
||||
}
|
||||
|
||||
private static Segments readSegments(Path window) throws IOException {
|
||||
Path json = jsonFor(window);
|
||||
if (!Files.isReadable(json)) {
|
||||
throw new IOException("whisper nie zapisał wyniku dla " + window.getFileName());
|
||||
}
|
||||
JsonObject root = JsonParser
|
||||
.parseString(Files.readString(json, StandardCharsets.UTF_8))
|
||||
.getAsJsonObject();
|
||||
|
||||
List<int[]> ranges = new ArrayList<>();
|
||||
List<String> texts = new ArrayList<>();
|
||||
for (JsonElement element : root.getAsJsonArray("transcription")) {
|
||||
JsonObject segment = element.getAsJsonObject();
|
||||
JsonObject offsets = segment.getAsJsonObject("offsets");
|
||||
ranges.add(new int[]{offsets.get("from").getAsInt(), offsets.get("to").getAsInt()});
|
||||
texts.add(segment.get("text").getAsString());
|
||||
}
|
||||
return new Segments(ranges, texts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bez {@code -of} whisper.cpp dopisuje rozszerzenie do pełnej nazwy wejścia:
|
||||
* {@code okno0.wav} → {@code okno0.wav.json}. Starsze wydania podmieniały je
|
||||
* zamiast dopisywać, więc sprawdzamy obie postacie.
|
||||
*/
|
||||
private static Path jsonFor(Path window) {
|
||||
String name = window.getFileName().toString();
|
||||
Path dopisane = window.resolveSibling(name + ".json");
|
||||
if (Files.isReadable(dopisane)) {
|
||||
return dopisane;
|
||||
}
|
||||
return window.resolveSibling(name.substring(0, name.lastIndexOf('.')) + ".json");
|
||||
}
|
||||
|
||||
private static void clean(List<Path> inputs) {
|
||||
for (Path input : inputs) {
|
||||
try {
|
||||
Files.deleteIfExists(input);
|
||||
Files.deleteIfExists(resultPath(input));
|
||||
Files.deleteIfExists(jsonFor(input));
|
||||
} catch (IOException ignored) {
|
||||
// katalog tymczasowy i tak zniknie na końcu
|
||||
}
|
||||
@@ -358,60 +596,47 @@ public final class Transcriber {
|
||||
return out.toString().trim();
|
||||
}
|
||||
|
||||
/** whisper.cpp dopisuje rozszerzenie do pełnej nazwy wejścia: klip3.wav → klip3.wav.txt. */
|
||||
private static Path resultPath(Path input) {
|
||||
return input.resolveSibling(input.getFileName() + ".txt");
|
||||
}
|
||||
|
||||
private static String readResult(Path input) throws IOException {
|
||||
Path result = resultPath(input);
|
||||
if (!Files.isReadable(result)) {
|
||||
// starsze wydania whisper.cpp podmieniały rozszerzenie zamiast dopisywać
|
||||
String name = input.getFileName().toString();
|
||||
result = input.resolveSibling(name.substring(0, name.lastIndexOf('.')) + ".txt");
|
||||
}
|
||||
return Files.isReadable(result)
|
||||
? Files.readString(result, StandardCharsets.UTF_8).trim()
|
||||
: "";
|
||||
}
|
||||
|
||||
/** ffmpeg przepróbkowuje do 16 kHz mono — whisper.cpp innego wejścia nie przyjmuje. */
|
||||
private Path convert(Path workDir, byte[] raw, int index) throws Exception {
|
||||
Path wav = workDir.resolve("klip" + index + ".wav");
|
||||
Files.deleteIfExists(wav);
|
||||
|
||||
/** ffmpeg przepróbkowuje do 16 kHz mono; surowe próbki sklejamy sami. */
|
||||
private byte[] toPcm(byte[] raw) throws Exception {
|
||||
Process ffmpeg = new ProcessBuilder("ffmpeg", "-hide_banner", "-loglevel", "error",
|
||||
"-i", "pipe:0", "-ar", String.valueOf(WHISPER_RATE), "-ac", "1",
|
||||
"-c:a", "pcm_s16le", "-y", wav.toString())
|
||||
.redirectErrorStream(true)
|
||||
"-f", "s16le", "pipe:1")
|
||||
.start();
|
||||
try (OutputStream in = ffmpeg.getOutputStream()) {
|
||||
in.write(raw);
|
||||
} catch (IOException ignored) {
|
||||
// ffmpeg potrafi zamknąć wejście wcześniej, gdy nagłówek mu wystarczy
|
||||
}
|
||||
String output = new String(ffmpeg.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
Thread feeder = new Thread(() -> {
|
||||
try (OutputStream in = ffmpeg.getOutputStream()) {
|
||||
in.write(raw);
|
||||
} catch (IOException ignored) {
|
||||
// ffmpeg potrafi zamknąć wejście, gdy nagłówek mu wystarczy
|
||||
}
|
||||
});
|
||||
feeder.setDaemon(true);
|
||||
feeder.start();
|
||||
|
||||
byte[] pcm = ffmpeg.getInputStream().readAllBytes();
|
||||
String errors = new String(ffmpeg.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
if (ffmpeg.waitFor() != 0) {
|
||||
throw new IOException("ffmpeg: " + output.trim());
|
||||
throw new IOException("ffmpeg: " + errors.trim());
|
||||
}
|
||||
return wav;
|
||||
feeder.join(1000);
|
||||
return pcm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jedno uruchomienie whisper.cpp na cały wsad. Wynik idzie do plików obok wejść
|
||||
* ({@code -otxt}), bo przy wielu plikach naraz nie da się rozdzielić tego, co
|
||||
* program wypisuje na standardowe wyjście.
|
||||
* Jedno uruchomienie whisper.cpp na kilka okien. Wynik zapisujemy w JSON-ie
|
||||
* ({@code -oj}), bo tylko tam są znaczniki czasu potrzebne do rozcięcia okna
|
||||
* z powrotem na kwestie.
|
||||
*/
|
||||
private void transcribe(Tool tool, List<Path> inputs, String lang) throws Exception {
|
||||
private void runWhisper(Tool tool, List<Path> inputs, String lang) throws Exception {
|
||||
List<String> command = new ArrayList<>(List.of(tool.binary(),
|
||||
"-m", tool.model(), "-l", lang, "-t", THREADS, "-nt", "-np", "-otxt"));
|
||||
"-m", tool.model(), "-l", lang, "-t", THREADS, "-np", "-oj"));
|
||||
inputs.forEach(path -> command.add(path.toString()));
|
||||
|
||||
Process whisper = new ProcessBuilder(command).start();
|
||||
whisper.getInputStream().readAllBytes();
|
||||
String errors = new String(whisper.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
if (whisper.waitFor() != 0) {
|
||||
throw new IOException("whisper: " + errors.trim());
|
||||
throw new IOException(errors.trim());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,7 +748,7 @@ public final class Transcriber {
|
||||
return;
|
||||
}
|
||||
try (var stream = Files.walk(directory)) {
|
||||
stream.sorted(java.util.Comparator.reverseOrder()).forEach(path -> {
|
||||
stream.sorted(Comparator.reverseOrder()).forEach(path -> {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException ignored) {
|
||||
|
||||
Reference in New Issue
Block a user