Rework to avoid using preferences object to retrieve options

This commit is contained in:
Guillaume Tâche
2024-09-22 21:59:10 +02:00
parent 7f99c48e2c
commit c59619da2d
115 changed files with 2294 additions and 765 deletions

View File

@@ -15,5 +15,9 @@
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper-common</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,8 +1,8 @@
package com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisper.base;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.parser.json.whisper.base.JSONSubtitleConverter;
import com.google.gson.Gson;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
@@ -25,7 +25,7 @@ public abstract class WhisperJsonModule {
@Provides
@Singleton
static Gson providesGson() {
return new Gson();
static ObjectMapper providesObjectMapper() {
return new ObjectMapper();
}
}

View File

@@ -5,7 +5,7 @@ import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverterProvider;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractOptions;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.whisper.AbstractWhisperSubtitleExtractor;
import com.github.gtache.autosubtitle.whisper.WhisperModels;
@@ -30,7 +30,9 @@ public class WhisperSubtitleExtractor extends AbstractWhisperSubtitleExtractor {
}
@Override
protected List<String> createArgs(final Path path, final Language language, final ExtractionModel model, final Path outputDir) {
protected List<String> createArgs(final Path path, final ExtractOptions options, final Path outputDir) {
final var model = options.model();
final var language = options.language();
final var args = new ArrayList<String>(14);
args.add(getPythonPath().toString());
args.add("-m");

View File

@@ -1,14 +1,17 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisper.base;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.VideoInfo;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.converter.FormatException;
import com.github.gtache.autosubtitle.subtitle.converter.FormatOptions;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.converter.ParseOptions;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import com.google.gson.Gson;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -28,26 +31,30 @@ import java.util.stream.Collectors;
public class JSONSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
private static final Logger logger = LogManager.getLogger(JSONSubtitleConverter.class);
private final Gson gson;
private final ObjectMapper mapper;
@Inject
JSONSubtitleConverter(final Gson gson) {
this.gson = Objects.requireNonNull(gson);
JSONSubtitleConverter(final ObjectMapper mapper) {
this.mapper = Objects.requireNonNull(mapper);
}
@Override
public String format(final SubtitleCollection<?> collection, final VideoInfo videoInfo) {
public String format(final SubtitleCollection<?> collection, final FormatOptions options) throws FormatException {
final var id = new AtomicInteger(0);
final var segments = collection.subtitles().stream().map(s -> new JSONSubtitleSegment(id.incrementAndGet(), 0, s.start() / (double) 1000,
s.end() / (double) 1000, s.content(), List.of(), 0, 0, 0, 0)).toList();
final var subtitles = new JSONSubtitles(collection.text(), segments, collection.language().iso2());
return gson.toJson(subtitles);
try {
return mapper.writeValueAsString(subtitles);
} catch (final JsonProcessingException e) {
throw new FormatException(e);
}
}
@Override
public SubtitleCollectionImpl<SubtitleImpl> parse(final String content) throws ParseException {
public SubtitleCollectionImpl<SubtitleImpl> parse(final String content, final ParseOptions options) throws ParseException {
try {
final var json = gson.fromJson(content, JSONSubtitles.class);
final var json = mapper.readValue(content, JSONSubtitles.class);
final var subtitles = json.segments().stream().map(s -> {
final var start = (long) (s.start() * 1000L);
final var end = (long) (s.end() * 1000L);

View File

@@ -3,9 +3,9 @@
*/
module com.github.gtache.autosubtitle.whisper.base {
requires transitive com.github.gtache.autosubtitle.whisper.common;
requires com.fasterxml.jackson.databind;
requires com.github.gtache.autosubtitle.core;
requires org.apache.logging.log4j;
requires com.google.gson;
exports com.github.gtache.autosubtitle.setup.whisper.base;
exports com.github.gtache.autosubtitle.subtitle.extractor.whisper.base;

View File

@@ -2,7 +2,6 @@ package com.github.gtache.autosubtitle.subtitle.extractor.whisper;
import com.github.gtache.autosubtitle.Audio;
import com.github.gtache.autosubtitle.File;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.process.ProcessRunner;
@@ -13,7 +12,7 @@ import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverterProvider;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractEvent;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractException;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractOptions;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractorListener;
import com.github.gtache.autosubtitle.subtitle.extractor.impl.ExtractEventImpl;
@@ -79,41 +78,41 @@ public abstract class AbstractWhisperSubtitleExtractor implements SubtitleExtrac
@Override
public SubtitleCollection<Subtitle> extract(final Video video, final Language language, final ExtractionModel model) throws ExtractException {
return extract(new AudioOrVideo(video), language, model);
public SubtitleCollection<Subtitle> extract(final Video video, final ExtractOptions options) throws ExtractException {
return extract(new AudioOrVideo(video), options);
}
@Override
public SubtitleCollection<Subtitle> extract(final Audio audio, final Language language, final ExtractionModel model) throws ExtractException {
return extract(new AudioOrVideo(audio), language, model);
public SubtitleCollection<Subtitle> extract(final Audio audio, final ExtractOptions options) throws ExtractException {
return extract(new AudioOrVideo(audio), options);
}
private SubtitleCollection<Subtitle> extract(final AudioOrVideo av, final Language language, final ExtractionModel model) throws ExtractException {
private SubtitleCollection<Subtitle> extract(final AudioOrVideo av, final ExtractOptions options) throws ExtractException {
if (av.inner() instanceof final File f) {
return extract(f.path(), language, model, av.info().duration());
return extract(f.path(), options, av.info().duration());
} else {
try {
return dumpExtract(av, language, model);
return dumpExtract(av, options);
} catch (final IOException e) {
throw new ExtractException(e);
}
}
}
private SubtitleCollection<Subtitle> dumpExtract(final AudioOrVideo av, final Language language, final ExtractionModel model) throws ExtractException, IOException {
private SubtitleCollection<Subtitle> dumpExtract(final AudioOrVideo av, final ExtractOptions options) throws ExtractException, IOException {
final var path = Files.createTempFile(AUTOSUBTITLE, "." + av.info().format());
try (final var in = av.getInputStream()) {
Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
return extract(path, language, model, av.info().duration());
return extract(path, options, av.info().duration());
} finally {
Files.deleteIfExists(path);
}
}
private SubtitleCollection<Subtitle> extract(final Path path, final Language language, final ExtractionModel model, final long duration) throws ExtractException {
private SubtitleCollection<Subtitle> extract(final Path path, final ExtractOptions options, final long duration) throws ExtractException {
try {
final var outputDir = Files.createTempDirectory(AUTOSUBTITLE);
final var args = createArgs(path, language, model, outputDir);
final var args = createArgs(path, options, outputDir);
final var processListener = processRunner.startListen(args);
var oldProgress = -1.0;
var line = processListener.readLine();
@@ -129,7 +128,7 @@ public abstract class AbstractWhisperSubtitleExtractor implements SubtitleExtrac
final var filename = path.getFileName().toString();
final var subtitleFilename = filename.substring(0, filename.lastIndexOf('.')) + ".json";
final var subtitleFile = outputDir.resolve(subtitleFilename);
return parseResult(subtitleFile);
return parseResult(subtitleFile, options);
} else {
throw new ExtractException("Error extracting subtitles: " + result.output());
}
@@ -138,9 +137,9 @@ public abstract class AbstractWhisperSubtitleExtractor implements SubtitleExtrac
}
}
private SubtitleCollection<Subtitle> parseResult(final Path subtitleFile) throws ExtractException {
private SubtitleCollection<Subtitle> parseResult(final Path subtitleFile, final ExtractOptions options) throws ExtractException {
try {
return converter.parse(subtitleFile);
return converter.parse(subtitleFile, options.parseOptions());
} catch (final ParseException e) {
throw new ExtractException(e);
}
@@ -175,12 +174,11 @@ public abstract class AbstractWhisperSubtitleExtractor implements SubtitleExtrac
* Creates the command line arguments for Whisper
*
* @param path the path to the file
* @param language the language
* @param model the model
* @param options the extraction options
* @param outputDir the output directory
* @return the list of arguments
*/
protected abstract List<String> createArgs(final Path path, final Language language, final ExtractionModel model, final Path outputDir);
protected abstract List<String> createArgs(final Path path, final ExtractOptions options, final Path outputDir);
/**
* @return the path to the python executable

View File

@@ -5,7 +5,6 @@ module com.github.gtache.autosubtitle.whisper.common {
requires transitive com.github.gtache.autosubtitle.conda;
requires transitive java.net.http;
requires org.apache.logging.log4j;
requires transitive com.google.gson;
requires transitive java.compiler; //Don't know why dagger generates @Generated here, need to debug
exports com.github.gtache.autosubtitle.whisper;

View File

@@ -2,7 +2,6 @@ package com.github.gtache.autosubtitle.subtitle.extractor.whisper;
import com.github.gtache.autosubtitle.Audio;
import com.github.gtache.autosubtitle.AudioInfo;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.VideoInfo;
import com.github.gtache.autosubtitle.impl.FileAudioImpl;
@@ -13,11 +12,12 @@ import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.converter.ParseOptions;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverterProvider;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractEvent;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractException;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractOptions;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractorListener;
import com.github.gtache.autosubtitle.subtitle.extractor.impl.ExtractEventImpl;
import org.junit.jupiter.api.BeforeEach;
@@ -52,15 +52,16 @@ class TestAbstractWhisperSubtitleExtractor {
private final AudioInfo audioInfo;
private final VideoInfo videoInfo;
private final ExtractionModel extractionModel;
private final ExtractOptions options;
private final ParseOptions parseOptions;
private final SubtitleCollection<Subtitle> collection;
TestAbstractWhisperSubtitleExtractor(@Mock final SubtitleConverterProvider converterProvider, @Mock final SubtitleConverter<Subtitle> converter,
@Mock final ProcessRunner processRunner, @Mock final ProcessListener processListener,
@Mock final ProcessResult processResult, @Mock final VideoInfo videoInfo,
@Mock final AudioInfo audioInfo, @Mock final ExtractionModel extractionModel,
@Mock final SubtitleCollection<Subtitle> collection) {
@Mock final AudioInfo audioInfo, @Mock final ExtractOptions options,
@Mock final ParseOptions parseOptions, @Mock final SubtitleCollection<Subtitle> collection) {
this.venvPath = Path.of("venv");
this.os = OS.LINUX;
this.converterProvider = Objects.requireNonNull(converterProvider);
@@ -70,7 +71,8 @@ class TestAbstractWhisperSubtitleExtractor {
this.processResult = Objects.requireNonNull(processResult);
this.audioInfo = Objects.requireNonNull(audioInfo);
this.videoInfo = Objects.requireNonNull(videoInfo);
this.extractionModel = Objects.requireNonNull(extractionModel);
this.options = Objects.requireNonNull(options);
this.parseOptions = Objects.requireNonNull(parseOptions);
this.collection = Objects.requireNonNull(collection);
}
@@ -81,6 +83,7 @@ class TestAbstractWhisperSubtitleExtractor {
when(processListener.join(Duration.ofHours(1))).thenReturn(processResult);
when(audioInfo.format()).thenReturn("mp3");
when(videoInfo.format()).thenReturn("mp4");
when(options.parseOptions()).thenReturn(parseOptions);
this.extractor = new DummyWhisperSubtitleExtractor(venvPath, converterProvider, processRunner, os);
}
@@ -126,7 +129,7 @@ class TestAbstractWhisperSubtitleExtractor {
final var audio = mock(Audio.class);
when(audio.info()).thenReturn(audioInfo);
when(audio.getInputStream()).thenThrow(IOException.class);
assertThrows(ExtractException.class, () -> extractor.extract(audio, Language.EN, extractionModel));
assertThrows(ExtractException.class, () -> extractor.extract(audio, options));
}
@Test
@@ -134,16 +137,16 @@ class TestAbstractWhisperSubtitleExtractor {
final var path = Paths.get("path");
final var audio = new FileAudioImpl(path, audioInfo);
doThrow(IOException.class).when(processListener).readLine();
assertThrows(ExtractException.class, () -> extractor.extract(audio, Language.EN, extractionModel));
assertThrows(ExtractException.class, () -> extractor.extract(audio, options));
}
@Test
void testExtractAudioFileParseException() throws ParseException {
final var path = Paths.get("path.path");
final var audio = new FileAudioImpl(path, audioInfo);
doThrow(ParseException.class).when(converter).parse(any(Path.class));
assertThrows(ExtractException.class, () -> extractor.extract(audio, Language.EN, extractionModel));
verify(converter).parse(any(Path.class));
doThrow(ParseException.class).when(converter).parse(any(Path.class), eq(parseOptions));
assertThrows(ExtractException.class, () -> extractor.extract(audio, options));
verify(converter).parse(any(Path.class), eq(parseOptions));
}
@Test
@@ -151,7 +154,7 @@ class TestAbstractWhisperSubtitleExtractor {
final var path = Paths.get("path");
final var audio = new FileAudioImpl(path, audioInfo);
when(processResult.exitCode()).thenReturn(1);
assertThrows(ExtractException.class, () -> extractor.extract(audio, Language.EN, extractionModel));
assertThrows(ExtractException.class, () -> extractor.extract(audio, options));
}
@Test
@@ -159,7 +162,7 @@ class TestAbstractWhisperSubtitleExtractor {
final var video = mock(Video.class);
when(video.info()).thenReturn(videoInfo);
when(video.getInputStream()).thenThrow(IOException.class);
assertThrows(ExtractException.class, () -> extractor.extract(video, Language.EN, extractionModel));
assertThrows(ExtractException.class, () -> extractor.extract(video, options));
}
@Test
@@ -168,8 +171,8 @@ class TestAbstractWhisperSubtitleExtractor {
when(video.info()).thenReturn(videoInfo);
final var in = new ByteArrayInputStream("test".getBytes());
when(video.getInputStream()).thenReturn(in);
when(converter.parse(any(Path.class))).thenReturn(collection);
assertEquals(collection, extractor.extract(video, Language.EN, extractionModel));
when(converter.parse(any(Path.class), eq(parseOptions))).thenReturn(collection);
assertEquals(collection, extractor.extract(video, options));
}
@Test
@@ -179,11 +182,11 @@ class TestAbstractWhisperSubtitleExtractor {
when(videoInfo.duration()).thenReturn(100000L);
final var in = new ByteArrayInputStream("test".getBytes());
when(video.getInputStream()).thenReturn(in);
when(converter.parse(any(Path.class))).thenReturn(collection);
when(converter.parse(any(Path.class), eq(parseOptions))).thenReturn(collection);
when(processListener.readLine()).thenReturn("Progress: 1.7abcd", "[00:12.234 --> 00:13.234] Hello", "98%|bbb", "abcd", null);
final var listener = mock(SubtitleExtractorListener.class);
extractor.addListener(listener);
assertEquals(collection, extractor.extract(video, Language.EN, extractionModel));
assertEquals(collection, extractor.extract(video, options));
verify(listener).listen(new ExtractEventImpl("Progress: 1.7abcd", 0.017));
verify(listener).listen(new ExtractEventImpl("[00:12.234 --> 00:13.234] Hello", 0.13234));
verify(listener).listen(new ExtractEventImpl("98%|bbb", 0.98));
@@ -208,8 +211,8 @@ class TestAbstractWhisperSubtitleExtractor {
}
@Override
protected List<String> createArgs(final Path path, final Language language, final ExtractionModel model, final Path outputDir) {
return List.of(path.toString(), language.toString(), model.toString(), outputDir.toString());
protected List<String> createArgs(final Path path, final ExtractOptions options, final Path outputDir) {
return List.of(path.toString(), options.toString(), outputDir.toString());
}
}
}

View File

@@ -38,15 +38,4 @@
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-conda</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -16,5 +16,9 @@
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper-common</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,8 +1,8 @@
package com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisperx;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.parser.json.whisperx.JSONSubtitleConverter;
import com.google.gson.Gson;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
@@ -25,7 +25,7 @@ public abstract class WhisperXJsonModule {
@Provides
@Singleton
static Gson providesGson() {
return new Gson();
static ObjectMapper providesObjectMapper() {
return new ObjectMapper();
}
}

View File

@@ -5,7 +5,7 @@ import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverterProvider;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractOptions;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.whisper.AbstractWhisperSubtitleExtractor;
import com.github.gtache.autosubtitle.whisper.WhisperModels;
@@ -28,7 +28,9 @@ public class WhisperXSubtitleExtractor extends AbstractWhisperSubtitleExtractor
}
@Override
protected List<String> createArgs(final Path path, final Language language, final ExtractionModel model, final Path outputDir) {
protected List<String> createArgs(final Path path, final ExtractOptions options, final Path outputDir) {
final var model = options.model();
final var language = options.language();
final var args = new ArrayList<String>();
args.add(getPythonPath().toString());
args.add("-m");

View File

@@ -1,16 +1,17 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisperx;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.VideoInfo;
import com.github.gtache.autosubtitle.modules.impl.MaxLineLength;
import com.github.gtache.autosubtitle.modules.impl.MaxLines;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.converter.FormatException;
import com.github.gtache.autosubtitle.subtitle.converter.FormatOptions;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.converter.ParseOptions;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import com.google.gson.Gson;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -19,7 +20,6 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.prefs.Preferences;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -30,42 +30,38 @@ import java.util.stream.Stream;
@Singleton
public class JSONSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
private static final String MAX_LINE_LENGTH = "maxLineLength";
private static final String MAX_LINES = "maxLines";
private static final Pattern SPLIT_PATTERN = Pattern.compile("[ \n]+");
private final Gson gson;
private final Preferences preferences;
private final int defaultMaxLineLength;
private final int defaultMaxLines;
private final ObjectMapper mapper;
@Inject
JSONSubtitleConverter(final Gson gson, final Preferences preferences, @MaxLineLength final int defaultMaxLineLength, @MaxLines final int defaultMaxLines) {
this.gson = Objects.requireNonNull(gson);
this.preferences = Objects.requireNonNull(preferences);
this.defaultMaxLineLength = defaultMaxLineLength;
this.defaultMaxLines = defaultMaxLines;
JSONSubtitleConverter(final ObjectMapper mapper) {
this.mapper = Objects.requireNonNull(mapper);
}
@Override
public String format(final SubtitleCollection<?> collection, final VideoInfo videoInfo) {
public String format(final SubtitleCollection<?> collection, final FormatOptions options) throws FormatException {
final var segments = collection.subtitles().stream().map(s -> new JSONSubtitleSegment(s.start() / (double) 1000,
s.end() / (double) 1000, s.content(), List.of())).toList();
final var subtitles = new JSONSubtitles(segments, collection.language().iso2());
return gson.toJson(subtitles);
try {
return mapper.writeValueAsString(subtitles);
} catch (final JsonProcessingException e) {
throw new FormatException(e);
}
}
@Override
public SubtitleCollectionImpl<SubtitleImpl> parse(final String content) throws ParseException {
public SubtitleCollectionImpl<SubtitleImpl> parse(final String content, final ParseOptions options) throws ParseException {
try {
final var json = gson.fromJson(content, JSONSubtitles.class);
final var json = mapper.readValue(content, JSONSubtitles.class);
final var subtitles = json.segments().stream().flatMap(s -> {
final var start = (long) (s.start() * 1000L);
final var end = (long) (s.end() * 1000L);
if (s.words().isEmpty()) {
return Stream.of(new SubtitleImpl(s.text(), start, end, null, null));
} else {
return splitSubtitle(s);
return splitSubtitle(s, options);
}
}).sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList();
@@ -77,23 +73,23 @@ public class JSONSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
}
}
private Stream<SubtitleImpl> splitSubtitle(final JSONSubtitleSegment segment) {
final var maxLineLength = preferences.getInt(MAX_LINE_LENGTH, defaultMaxLineLength);
final var maxLines = preferences.getInt(MAX_LINES, defaultMaxLines);
private static Stream<SubtitleImpl> splitSubtitle(final JSONSubtitleSegment segment, final ParseOptions options) {
final var maxLineLength = options.maxLineLength();
final var maxLines = options.maxLines();
final var text = segment.text();
if (text.length() <= maxLineLength) {
final var start = (long) (segment.start() * 1000L);
final var end = (long) (segment.end() * 1000L);
return Stream.of(new SubtitleImpl(text.replace("\n", " "), start, end, null, null));
} else if (text.length() <= maxLines * maxLineLength) {
return splitSubtitleLines(segment);
return splitSubtitleLines(segment, options);
} else {
return splitSubtitleWords(segment);
return splitSubtitleWords(segment, options);
}
}
private Stream<SubtitleImpl> splitSubtitleLines(final JSONSubtitleSegment segment) {
final var maxLineLength = preferences.getInt(MAX_LINE_LENGTH, defaultMaxLineLength);
private static Stream<SubtitleImpl> splitSubtitleLines(final JSONSubtitleSegment segment, final ParseOptions options) {
final var maxLineLength = options.maxLineLength();
final var text = segment.text();
final var split = SPLIT_PATTERN.split(text);
final var builder = new StringBuilder(text.length());
@@ -116,9 +112,9 @@ public class JSONSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
return currentLength / (maxLength + 1) < newLength / (maxLength + 1);
}
private Stream<SubtitleImpl> splitSubtitleWords(final JSONSubtitleSegment segment) {
final var maxLineLength = preferences.getInt(MAX_LINE_LENGTH, defaultMaxLineLength);
final var maxLines = preferences.getInt(MAX_LINES, defaultMaxLines);
private static Stream<SubtitleImpl> splitSubtitleWords(final JSONSubtitleSegment segment, final ParseOptions options) {
final var maxLineLength = options.maxLineLength();
final var maxLines = options.maxLines();
final var ret = new ArrayList<SubtitleImpl>(segment.text().length() / (maxLines * maxLineLength));
final var builder = new StringBuilder(maxLines * maxLineLength);
final var words = segment.words();

View File

@@ -1,6 +1,9 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisperx;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
@JsonIgnoreProperties("word_segments")
public record JSONSubtitles(List<JSONSubtitleSegment> segments, String language) {
}

View File

@@ -3,8 +3,8 @@
*/
module com.github.gtache.autosubtitle.whisperx {
requires transitive com.github.gtache.autosubtitle.whisper.common;
requires transitive com.fasterxml.jackson.databind;
requires org.apache.logging.log4j;
requires java.prefs;
exports com.github.gtache.autosubtitle.whisperx;
exports com.github.gtache.autosubtitle.setup.whisperx;

View File

@@ -1,6 +1,6 @@
package com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisperx;
import com.google.gson.Gson;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -8,7 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf;
class TestWhisperXJsonModule {
@Test
void testGson() {
assertInstanceOf(Gson.class, WhisperXJsonModule.providesGson());
void testObjectMapper() {
assertInstanceOf(ObjectMapper.class, WhisperXJsonModule.providesObjectMapper());
}
}

View File

@@ -6,6 +6,7 @@ import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverterProvider;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractOptions;
import com.github.gtache.autosubtitle.whisper.WhisperModels;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -16,11 +17,12 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestWhisperXSubtitleExtractor {
@@ -29,14 +31,16 @@ class TestWhisperXSubtitleExtractor {
private final SubtitleConverterProvider converterProvider;
private final SubtitleConverter<Subtitle> converter;
private final ProcessRunner processRunner;
private final ExtractOptions options;
private final OS os;
private WhisperXSubtitleExtractor whisperXSubtitleExtractor;
TestWhisperXSubtitleExtractor(@Mock final SubtitleConverterProvider converterProvider, @Mock final SubtitleConverter<Subtitle> converter,
@Mock final ProcessRunner processRunner) {
this.converterProvider = Objects.requireNonNull(converterProvider);
this.converter = Objects.requireNonNull(converter);
this.processRunner = Objects.requireNonNull(processRunner);
@Mock final ProcessRunner processRunner, @Mock final ExtractOptions options) {
this.converterProvider = requireNonNull(converterProvider);
this.converter = requireNonNull(converter);
this.processRunner = requireNonNull(processRunner);
this.options = requireNonNull(options);
this.venvPath = Paths.get("path");
this.os = OS.LINUX;
}
@@ -51,44 +55,44 @@ class TestWhisperXSubtitleExtractor {
void testEN() {
final var path = Paths.get("in");
final var outputPath = Paths.get("out");
final var language = Language.EN;
final var model = WhisperModels.MEDIUM;
when(options.model()).thenReturn(WhisperModels.MEDIUM);
when(options.language()).thenReturn(Language.EN);
final var expected = List.of(venvPath.resolve("python").toString(),
"-m", "whisperx", "--verbose", "False", "--model", "medium.en", "--task", "transcribe",
"--output_dir", outputPath.toString(), "--output_format", "json", "--device", "cpu",
"--condition_on_previous_text", "True", "--print_progress", "True", "--compute_type",
"int8", "--threads", String.valueOf(Runtime.getRuntime().availableProcessors()), "--language",
"en", path.toString());
assertEquals(expected, whisperXSubtitleExtractor.createArgs(path, language, model, outputPath));
assertEquals(expected, whisperXSubtitleExtractor.createArgs(path, options, outputPath));
}
@Test
void testENLarge() {
final var path = Paths.get("in");
final var outputPath = Paths.get("out");
final var language = Language.EN;
final var model = WhisperModels.LARGE;
when(options.model()).thenReturn(WhisperModels.LARGE);
when(options.language()).thenReturn(Language.EN);
final var expected = List.of(venvPath.resolve("python").toString(),
"-m", "whisperx", "--verbose", "False", "--model", "large", "--task", "transcribe",
"--output_dir", outputPath.toString(), "--output_format", "json", "--device", "cpu",
"--condition_on_previous_text", "True", "--print_progress", "True", "--compute_type",
"int8", "--threads", String.valueOf(Runtime.getRuntime().availableProcessors()), "--language",
"en", path.toString());
assertEquals(expected, whisperXSubtitleExtractor.createArgs(path, language, model, outputPath));
assertEquals(expected, whisperXSubtitleExtractor.createArgs(path, options, outputPath));
}
@Test
void testAuto() {
final var path = Paths.get("in");
final var outputPath = Paths.get("out");
final var language = Language.AUTO;
final var model = WhisperModels.SMALL;
when(options.model()).thenReturn(WhisperModels.SMALL);
when(options.language()).thenReturn(Language.AUTO);
final var expected = List.of(venvPath.resolve("python").toString(),
"-m", "whisperx", "--verbose", "False", "--model", "small", "--task", "transcribe",
"--output_dir", outputPath.toString(), "--output_format", "json", "--device", "cpu",
"--condition_on_previous_text", "True", "--print_progress", "True", "--compute_type",
"int8", "--threads", String.valueOf(Runtime.getRuntime().availableProcessors()), path.toString());
assertEquals(expected, whisperXSubtitleExtractor.createArgs(path, language, model, outputPath));
assertEquals(expected, whisperXSubtitleExtractor.createArgs(path, options, outputPath));
}
@Test

View File

@@ -1,44 +1,48 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisperx;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.converter.FormatException;
import com.github.gtache.autosubtitle.subtitle.converter.FormatOptions;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.converter.ParseOptions;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import com.google.gson.Gson;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.prefs.Preferences;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TestJSONSubtitleConverter {
private final Preferences preferences;
private final int defaultMaxLineLength;
private final int defaultMaxLines;
private JSONSubtitleConverter converter;
private final ParseOptions parseOptions;
private final FormatOptions formatOptions;
private final JSONSubtitleConverter converter;
TestJSONSubtitleConverter() {
this.preferences = mock(Preferences.class);
this.defaultMaxLineLength = 100;
this.defaultMaxLines = 2;
TestJSONSubtitleConverter(@Mock final ParseOptions parseOptions, @Mock final FormatOptions formatOptions) {
this.parseOptions = Objects.requireNonNull(parseOptions);
this.formatOptions = Objects.requireNonNull(formatOptions);
this.converter = new JSONSubtitleConverter(new ObjectMapper());
}
@BeforeEach
void beforeEach() {
when(preferences.getInt("maxLineLength", defaultMaxLineLength)).thenReturn(defaultMaxLineLength);
when(preferences.getInt("maxLines", defaultMaxLines)).thenReturn(defaultMaxLines);
this.converter = new JSONSubtitleConverter(new Gson(), preferences, defaultMaxLineLength, defaultMaxLines);
when(parseOptions.maxLineLength()).thenReturn(100);
when(parseOptions.maxLines()).thenReturn(2);
}
@Test
@@ -47,7 +51,7 @@ class TestJSONSubtitleConverter {
}
@Test
void testParseFormat() throws IOException, ParseException {
void testParseFormat() throws IOException, ParseException, FormatException {
try (final var inStream = getClass().getResourceAsStream("whisperx-in.json");
final var outStream = getClass().getResourceAsStream("whisperx-out.json")) {
if (inStream == null || outStream == null) {
@@ -56,8 +60,8 @@ class TestJSONSubtitleConverter {
final var in = new String(inStream.readAllBytes(), StandardCharsets.UTF_8);
final var out = new String(outStream.readAllBytes(), StandardCharsets.UTF_8);
final var expected = new SubtitleCollectionImpl<Subtitle>("This is a test. Yes.", List.of(new SubtitleImpl("This is a test.", 9, 410, null, null), new SubtitleImpl("Yes.", 450, 6963, null, null)), Language.FR);
assertEquals(expected, converter.parse(in));
assertEquals(out, converter.format(expected, null));
assertEquals(expected, converter.parse(in, parseOptions));
assertEquals(out, converter.format(expected, formatOptions));
}
}
@@ -72,7 +76,7 @@ class TestJSONSubtitleConverter {
List.of(new SubtitleImpl("aaaaaaaaaa bbbbbbbbbb cccccccccc dddddddddd eeeeeeeeee ffffffffff gggggggggg hhhhhhhhhh iiiiiiiiii\njjjjjjjjjj kkkkkkkkkk llllllllll mmmmmmmmmm nnnnnnnnnn oooooooooo pppppppppp qqqqqqqqqq rrrrrrrrrr", 0, 18000, null, null),
new SubtitleImpl("ssssssssss tttttttttt uuuuuuuuuu vvvvvvvvvv wwwwwwwwww xxxxxxxxxx yyyyyyyyyy zzzzzzzzzz", 18000, 26000, null, null),
new SubtitleImpl("Yes.", 30000, 31000, null, null)), Language.EN);
assertEquals(expected, converter.parse(in));
assertEquals(expected, converter.parse(in, parseOptions));
}
}
@@ -86,7 +90,7 @@ class TestJSONSubtitleConverter {
final var expected = new SubtitleCollectionImpl<Subtitle>("aaaaaaaaaa bbbbbbbbbb cccccccccc dddddddddd eeeeeeeeee ffffffffff gggggggggg hhhhhhhhhh iiiiiiiiii\njjjjjjjjjj kkkkkkkkkk llllllllll Yes.",
List.of(new SubtitleImpl("aaaaaaaaaa bbbbbbbbbb cccccccccc dddddddddd eeeeeeeeee ffffffffff gggggggggg hhhhhhhhhh iiiiiiiiii\njjjjjjjjjj kkkkkkkkkk llllllllll", 0, 18000, null, null),
new SubtitleImpl("Yes.", 30000, 31000, null, null)), Language.EN);
assertEquals(expected, converter.parse(in));
assertEquals(expected, converter.parse(in, parseOptions));
}
}