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

@@ -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));
}
}