Allows setting max lines and max line length, enables DeepL, adds user setup bridge

This commit is contained in:
Guillaume Tâche
2024-08-20 21:31:10 +02:00
parent 273a6e996f
commit 44c317f207
49 changed files with 752 additions and 298 deletions

1
.gitignore vendored
View File

@@ -39,3 +39,4 @@ build/
.DS_Store
tools
.secrets

View File

@@ -1,55 +0,0 @@
package com.github.gtache.autosubtitle;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
/**
* Translates texts and subtitles
*/
public interface Translator<T extends Subtitle> {
/**
* Guesses the language of the given text
*
* @param text The text
* @return The guessed language
*/
Language getLanguage(final String text);
/**
* Guesses the language of the given subtitle
*
* @param subtitle The subtitle
* @return The guessed language
*/
default Language getLanguage(final Subtitle subtitle) {
return getLanguage(subtitle.content());
}
/**
* Translates the given text to the given language
*
* @param text The text to translate
* @param to The target language
* @return The translated text
*/
String translate(String text, Language to);
/**
* Translates the given subtitle to the given language
*
* @param subtitle The subtitle to translate
* @param to The target language
* @return The translated subtitle
*/
T translate(Subtitle subtitle, Language to);
/**
* Translates the given subtitles collection to the given language
*
* @param collection The subtitles collection to translate
* @param to The target language
* @return The translated subtitles collection
*/
SubtitleCollection<T> translate(SubtitleCollection<?> collection, Language to);
}

View File

@@ -0,0 +1,35 @@
package com.github.gtache.autosubtitle.setup;
import java.util.List;
/**
* Bridge between setup manager and the user
*/
public interface SetupUserBridge {
/**
* Asks for user input
*
* @param question the question to display to the user
* @return the user input
*/
String askForUserInput(String question);
/**
* Asks for user choice
*
* @param question the question to display to the user
* @param choices the possible choices
* @param <T> the type of the choices
* @return the user choice
*/
<T> T askForUserChoice(String question, List<T> choices);
/**
* Asks for user confirmation
*
* @param question the question to display to the user
* @return whether the user confirmed
*/
boolean askForUserConfirmation(String question);
}

View File

@@ -0,0 +1,19 @@
package com.github.gtache.autosubtitle.translation;
/**
* Exception thrown when an error occurs during translation
*/
public class TranslationException extends Exception {
public TranslationException(final String message) {
super(message);
}
public TranslationException(final String message, final Throwable cause) {
super(message, cause);
}
public TranslationException(final Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,98 @@
package com.github.gtache.autosubtitle.translation;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
/**
* Translates texts and subtitles
*/
public interface Translator<T extends Subtitle> {
/**
* Guesses the language of the given text
*
* @param text The text
* @return The guessed language
*/
Language getLanguage(final String text);
/**
* Guesses the language of the given subtitle
*
* @param subtitle The subtitle
* @return The guessed language
*/
default Language getLanguage(final Subtitle subtitle) {
return getLanguage(subtitle.content());
}
/**
* Translates the given text to the given language
*
* @param text The text to translate
* @param to The target language
* @return The translated text
* @throws TranslationException if an error occurred during the translation
*/
default String translate(final String text, final Language to) throws TranslationException {
return translate(text, getLanguage(text), to);
}
/**
* Translates the given text to the given language
*
* @param text The text to translate
* @param from The source language
* @param to The target language
* @return The translated text
* @throws TranslationException if an error occurred during the translation
*/
String translate(String text, Language from, Language to) throws TranslationException;
/**
* Translates the given subtitle to the given language
*
* @param subtitle The subtitle to translate
* @param to The target language
* @return The translated subtitle
* @throws TranslationException if an error occurred during the translation
*/
default T translate(final Subtitle subtitle, final Language to) throws TranslationException {
return translate(subtitle, getLanguage(subtitle), to);
}
/**
* Translates the given subtitle to the given language
*
* @param subtitle The subtitle to translate
* @param from The source language
* @param to The target language
* @return The translated subtitle
* @throws TranslationException if an error occurred during the translation
*/
T translate(Subtitle subtitle, Language from, Language to) throws TranslationException;
/**
* Translates the given subtitles collection to the given language
*
* @param collection The subtitles collection to translate
* @param to The target language
* @return The translated subtitles collection
* @throws TranslationException if an error occurred during the translation
*/
default SubtitleCollection<T> translate(final SubtitleCollection<?> collection, final Language to) throws TranslationException {
return translate(collection, getLanguage(collection.text()), to);
}
/**
* Translates the given subtitles collection to the given language
*
* @param collection The subtitles collection to translate
* @param from The source language
* @param to The target language
* @return The translated subtitles collection
* @throws TranslationException if an error occurred during the translation
*/
SubtitleCollection<T> translate(SubtitleCollection<?> collection, Language from, Language to) throws TranslationException;
}

View File

@@ -9,4 +9,5 @@ module com.github.gtache.autosubtitle.api {
exports com.github.gtache.autosubtitle.subtitle;
exports com.github.gtache.autosubtitle.subtitle.extractor;
exports com.github.gtache.autosubtitle.subtitle.converter;
exports com.github.gtache.autosubtitle.translation;
}

View File

@@ -1,6 +1,7 @@
package com.github.gtache.autosubtitle;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.translation.Translator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;

View File

@@ -13,6 +13,9 @@ import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import javax.inject.Singleton;
import java.util.prefs.Preferences;
/**
* Dagger module for Core
*/
@@ -53,4 +56,22 @@ public abstract class CoreModule {
static String providesExecutableExtension(final OS os) {
return os == OS.WINDOWS ? ".exe" : "";
}
@Provides
@Singleton
static Preferences providesPreferences() {
return Preferences.userRoot().node("/com/github/gtache/autosubtitle");
}
@Provides
@MaxLineLength
static int providesMaxLineLength() {
return 40;
}
@Provides
@MaxLines
static int providesMaxLines() {
return 1;
}
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.impl;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface MaxLineLength {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.impl;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface MaxLines {
}

View File

@@ -1,12 +1,12 @@
package com.github.gtache.autosubtitle.subtitle.converter.impl;
import com.github.gtache.autosubtitle.Translator;
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.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import com.github.gtache.autosubtitle.translation.Translator;
import javax.inject.Inject;
import java.util.Arrays;

View File

@@ -7,6 +7,7 @@ module com.github.gtache.autosubtitle.core {
requires transitive java.net.http;
requires transitive javax.inject;
requires org.apache.logging.log4j;
requires java.prefs;
exports com.github.gtache.autosubtitle.impl;
exports com.github.gtache.autosubtitle.archive.impl;

View File

@@ -1,10 +1,10 @@
package com.github.gtache.autosubtitle.subtitle.converter.impl;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import com.github.gtache.autosubtitle.translation.Translator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;

View File

@@ -11,11 +11,26 @@
<artifactId>autosubtitle-deepl</artifactId>
<properties>
<deepl.version>1.5.0</deepl.version>
<lingua.version>1.2.2</lingua.version>
</properties>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-core</artifactId>
</dependency>
<dependency>
<groupId>com.deepl.api</groupId>
<artifactId>deepl-java</artifactId>
<version>${deepl.version}</version>
</dependency>
<dependency>
<groupId>com.github.pemistahl</groupId>
<artifactId>lingua</artifactId>
<version>${lingua.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,38 +0,0 @@
package com.github.gtache.autosubtitle.deepl;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import javax.inject.Inject;
/**
* DeepL implementation of {@link Translator}
*/
public class DeepLTranslator implements Translator<Subtitle> {
@Inject
DeepLTranslator() {
}
@Override
public Language getLanguage(final String text) {
return null;
}
@Override
public String translate(final String text, final Language to) {
return "";
}
@Override
public Subtitle translate(final Subtitle subtitle, final Language to) {
return null;
}
@Override
public SubtitleCollection<Subtitle> translate(final SubtitleCollection<?> collection, final Language to) {
return null;
}
}

View File

@@ -1,16 +1,28 @@
package com.github.gtache.autosubtitle.modules.deepl;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.deepl.DeepLTranslator;
import com.github.gtache.autosubtitle.modules.setup.deepl.DeepLSetupModule;
import com.github.gtache.autosubtitle.translation.Translator;
import com.github.gtache.autosubtitle.translation.deepl.DeepLTranslator;
import com.github.pemistahl.lingua.api.LanguageDetector;
import com.github.pemistahl.lingua.api.LanguageDetectorBuilder;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
/**
* Dagger module for DeepL
*/
@Module
public interface DeepLModule {
@Module(includes = DeepLSetupModule.class)
public abstract class DeepLModule {
@Binds
Translator bindsTranslator(final DeepLTranslator translator);
abstract Translator bindsTranslator(final DeepLTranslator translator);
@Provides
@Singleton
static LanguageDetector providesLanguageDetector() {
return LanguageDetectorBuilder.fromAllSpokenLanguages().build();
}
}

View File

@@ -0,0 +1,21 @@
package com.github.gtache.autosubtitle.modules.setup.deepl;
import com.github.gtache.autosubtitle.modules.setup.impl.TranslatorSetup;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.deepl.DeepLSetupManager;
import dagger.Binds;
import dagger.Module;
/**
* Dagger module for DeepL setup
*/
@Module
public abstract class DeepLSetupModule {
private DeepLSetupModule() {
}
@Binds
@TranslatorSetup
abstract SetupManager bindsSetupManager(final DeepLSetupManager setupManager);
}

View File

@@ -0,0 +1,70 @@
package com.github.gtache.autosubtitle.setup.deepl;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.SetupStatus;
import com.github.gtache.autosubtitle.setup.SetupUserBridge;
import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
import javax.inject.Inject;
import java.util.Objects;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
/**
* {@link SetupManager} for DeepL
*/
public class DeepLSetupManager extends AbstractSetupManager {
private static final String DEEPL_API_KEY = "deepl.api.key";
private final SetupUserBridge userBridge;
private final Preferences preferences;
@Inject
DeepLSetupManager(final SetupUserBridge userBridge, final Preferences preferences) {
this.userBridge = Objects.requireNonNull(userBridge);
this.preferences = Objects.requireNonNull(preferences);
}
@Override
protected SetupStatus getStatus() throws SetupException {
final var key = preferences.get(DEEPL_API_KEY, null);
return key == null ? SetupStatus.NOT_INSTALLED : SetupStatus.BUNDLE_INSTALLED;
}
@Override
public String name() {
return "DeepL";
}
@Override
public void install() throws SetupException {
final var key = userBridge.askForUserInput("Please enter your DeepL API key - https://www.deepl.com/pro-api? (It will be stored unencrypted)");
if (key == null) {
throw new SetupException("API key cannot be null");
} else {
preferences.put(DEEPL_API_KEY, key);
try {
preferences.flush();
} catch (final BackingStoreException e) {
throw new SetupException(e);
}
}
}
@Override
public void uninstall() throws SetupException {
preferences.remove(DEEPL_API_KEY);
try {
preferences.flush();
} catch (final BackingStoreException e) {
throw new SetupException(e);
}
}
@Override
public void update() throws SetupException {
//No need to update
}
}

View File

@@ -0,0 +1,78 @@
package com.github.gtache.autosubtitle.translation.deepl;
import com.deepl.api.DeepLException;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import com.github.gtache.autosubtitle.translation.TranslationException;
import com.github.gtache.autosubtitle.translation.Translator;
import com.github.pemistahl.lingua.api.LanguageDetector;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Objects;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;
/**
* DeepL implementation of {@link Translator}
*/
public class DeepLTranslator implements Translator<Subtitle> {
private final Preferences preferences;
private final LanguageDetector languageDetector;
private com.deepl.api.Translator translator;
@Inject
DeepLTranslator(final Preferences preferences, final LanguageDetector languageDetector) {
this.preferences = Objects.requireNonNull(preferences);
this.languageDetector = Objects.requireNonNull(languageDetector);
}
@Override
public Language getLanguage(final String text) {
return Language.getLanguage(languageDetector.detectLanguageOf(text).getIsoCode639_1().toString());
}
@Override
public String translate(final String text, final Language from, final Language to) throws TranslationException {
final var currentTranslator = getTranslator();
if (currentTranslator == null) {
throw new TranslationException("DeepL API key is not set");
}
try {
final var translated = currentTranslator.translateText(text, from.iso2(), to.iso2());
return translated.getText();
} catch (final DeepLException e) {
throw new TranslationException(e);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new TranslationException(e);
}
}
@Override
public Subtitle translate(final Subtitle subtitle, final Language from, final Language to) throws TranslationException {
final var translated = translate(subtitle.content(), from, to);
return new SubtitleImpl(translated, subtitle.start(), subtitle.end(), subtitle.font(), subtitle.bounds());
}
@Override
public SubtitleCollection<Subtitle> translate(final SubtitleCollection<?> collection, final Language from, final Language to) throws TranslationException {
final var subtitles = new ArrayList<Subtitle>(collection.subtitles().size());
for (final var subtitle : collection.subtitles()) {
subtitles.add(translate(subtitle, from, to));
}
final var text = subtitles.stream().map(Subtitle::content).collect(Collectors.joining(""));
return new SubtitleCollectionImpl<>(text, subtitles, to);
}
private com.deepl.api.Translator getTranslator() {
if (translator == null && preferences.get("deepl.api.key", null) != null) {
translator = new com.deepl.api.Translator(preferences.get("deepl.api.key", null));
}
return translator;
}
}

View File

@@ -3,6 +3,10 @@
*/
module com.github.gtache.autosubtitle.deepl {
requires transitive com.github.gtache.autosubtitle.core;
exports com.github.gtache.autosubtitle.deepl;
requires transitive deepl.java;
requires transitive com.github.pemistahl.lingua;
requires transitive java.prefs;
exports com.github.gtache.autosubtitle.modules.deepl;
exports com.github.gtache.autosubtitle.translation.deepl;
exports com.github.gtache.autosubtitle.setup.deepl;
}

View File

@@ -64,4 +64,12 @@ public interface ParametersModel {
* @param fontSize The new font size
*/
void setFontSize(int fontSize);
int maxLineLength();
void setMaxLineLength(int maxLineLength);
int maxLines();
void setMaxLines(int maxLines);
}

View File

@@ -3,4 +3,6 @@ parameters.button.save.label=Save
parameters.extraction.model.label=Model to use for subtitle extraction
parameters.subtitles.font.family=Default subtitle font name
parameters.subtitles.font.size=Default subtitle font size
parameters.subtitles.max.length.label=Max length of a subtitles line (characters)
parameters.subtitles.max.lines.label=Max subtitles lines
parameters.subtitles.output.format=Output format for the subtitles

View File

@@ -3,4 +3,5 @@ parameters.button.save.label=Sauvegarder
parameters.extraction.model.label=Mod\u00E8le utilis\u00E9 pour l'extraction des sous-titres
parameters.subtitles.font.family=Police par d\u00E9faut pour les sous-titres
parameters.subtitles.font.size=Taille de la police par d\u00E9faut pour les sous-titres
parameters.subtitles.max.length.label=Taille maximale d'une ligne de sous-titres (caract\u00E8res)
parameters.subtitles.output.format=Format de sortie pour les sous-titres

View File

@@ -12,7 +12,7 @@ public abstract class AbstractFXController {
/**
* @return the current window
*/
protected abstract Window window();
public abstract Window window();
/**
* Show an error dialog

View File

@@ -42,7 +42,7 @@ public class FXMainController extends AbstractFXController implements MainContro
}
@Override
protected Window window() {
public Window window() {
return tabPane.getScene().getWindow();
}
}

View File

@@ -37,6 +37,10 @@ public class FXParametersController extends AbstractFXController implements Para
private PrefixSelectionComboBox<String> fontFamilyCombobox;
@FXML
private TextField fontSizeField;
@FXML
private TextField maxLengthField;
@FXML
private TextField maxLinesField;
private final FXParametersModel model;
private final Preferences preferences;
@@ -68,8 +72,12 @@ public class FXParametersController extends AbstractFXController implements Para
return null;
};
fontSizeField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter(), 0, integerFilter));
maxLengthField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter(), 0, integerFilter));
maxLinesField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter(), 0, integerFilter));
fontSizeField.textProperty().bindBidirectional(model.fontSizeProperty(), new NumberStringConverter());
maxLengthField.textProperty().bindBidirectional(model.maxLineLengthProperty(), new NumberStringConverter());
maxLinesField.textProperty().bindBidirectional(model.maxLinesProperty(), new NumberStringConverter());
loadPreferences();
}
@@ -79,11 +87,15 @@ public class FXParametersController extends AbstractFXController implements Para
final var outputFormat = preferences.get("outputFormat", model.outputFormat().name());
final var fontFamily = preferences.get("fontFamily", model.fontFamily());
final var fontSize = preferences.getInt("fontSize", model.fontSize());
final var maxLineLength = preferences.getInt("maxLineLength", model.maxLineLength());
final var maxLines = preferences.getInt("maxLines", model.maxLines());
model.setExtractionModel(extractionModelProvider.getExtractionModel(extractionModel));
model.setOutputFormat(OutputFormat.valueOf(outputFormat));
model.setFontFamily(fontFamily);
model.setFontSize(fontSize);
model.setMaxLineLength(maxLineLength);
model.setMaxLines(maxLines);
logger.info("Loaded preferences");
}
@@ -95,6 +107,8 @@ public class FXParametersController extends AbstractFXController implements Para
preferences.put("outputFormat", model.outputFormat().name());
preferences.put("fontFamily", model.fontFamily());
preferences.putInt("fontSize", model.fontSize());
preferences.putInt("maxLineLength", model.maxLineLength());
preferences.putInt("maxLines", model.maxLines());
try {
preferences.flush();
logger.info("Preferences saved");
@@ -124,7 +138,7 @@ public class FXParametersController extends AbstractFXController implements Para
}
@Override
protected Window window() {
public Window window() {
return extractionModelCombobox.getScene().getWindow();
}
}

View File

@@ -3,6 +3,8 @@ package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.ParametersModel;
import com.github.gtache.autosubtitle.modules.gui.impl.FontFamily;
import com.github.gtache.autosubtitle.modules.gui.impl.FontSize;
import com.github.gtache.autosubtitle.modules.impl.MaxLineLength;
import com.github.gtache.autosubtitle.modules.impl.MaxLines;
import com.github.gtache.autosubtitle.subtitle.OutputFormat;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
@@ -31,9 +33,12 @@ public class FXParametersModel implements ParametersModel {
private final ObservableList<String> availableFontFamilies;
private final StringProperty fontFamily;
private final IntegerProperty fontSize;
private final IntegerProperty maxLineLength;
private final IntegerProperty maxLines;
@Inject
FXParametersModel(final ExtractionModelProvider extractionModelProvider, @FontFamily final String defaultFontFamily, @FontSize final int defaultFontSize) {
FXParametersModel(final ExtractionModelProvider extractionModelProvider, @FontFamily final String defaultFontFamily,
@FontSize final int defaultFontSize, @MaxLineLength final int defaultMaxLineLength, @MaxLines final int defaultMaxLines) {
this.availableExtractionModels = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(extractionModelProvider.getAvailableExtractionModels()));
this.extractionModel = new SimpleObjectProperty<>(extractionModelProvider.getDefaultExtractionModel());
this.availableOutputFormats = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(OutputFormat.SRT));
@@ -41,6 +46,8 @@ public class FXParametersModel implements ParametersModel {
this.availableFontFamilies = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList("Arial"));
this.fontFamily = new SimpleStringProperty(defaultFontFamily);
this.fontSize = new SimpleIntegerProperty(defaultFontSize);
this.maxLineLength = new SimpleIntegerProperty(defaultMaxLineLength);
this.maxLines = new SimpleIntegerProperty(defaultMaxLines);
}
@Override
@@ -113,4 +120,32 @@ public class FXParametersModel implements ParametersModel {
IntegerProperty fontSizeProperty() {
return fontSize;
}
@Override
public int maxLineLength() {
return maxLineLength.get();
}
@Override
public void setMaxLineLength(final int maxLineLength) {
this.maxLineLength.set(maxLineLength);
}
IntegerProperty maxLineLengthProperty() {
return maxLineLength;
}
@Override
public int maxLines() {
return maxLines.get();
}
@Override
public void setMaxLines(final int maxLines) {
this.maxLines.set(maxLines);
}
IntegerProperty maxLinesProperty() {
return maxLines;
}
}

View File

@@ -314,7 +314,7 @@ public class FXSetupController extends AbstractFXController implements SetupCont
}
@Override
protected Window window() {
public Window window() {
return converterNameLabel.getScene().getWindow();
}
}

View File

@@ -1,7 +1,6 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.gui.SubtitlesController;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
@@ -9,6 +8,8 @@ import com.github.gtache.autosubtitle.subtitle.SubtitleImporterExporter;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl;
import com.github.gtache.autosubtitle.translation.TranslationException;
import com.github.gtache.autosubtitle.translation.Translator;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
@@ -39,6 +40,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import static java.util.Objects.requireNonNull;
@@ -129,19 +131,30 @@ public class FXSubtitlesController extends AbstractFXController implements Subti
}
});
translationsCombobox.valueProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null && !model.collections().containsKey(newValue)) {
translationsCombobox.setOnAction(e -> {
final var value = translationsCombobox.getValue();
if (value != null && !model.collections().containsKey(value)) {
model.setTranslating(true);
CompletableFuture.supplyAsync(() -> translator.translate(model.collections().get(model.videoLanguage()), newValue))
.whenCompleteAsync((r, t) -> {
if (t == null) {
loadCollection(r);
model.setSelectedCollection(model.collections().get(newValue));
} else {
logger.error("Error while translating to {}", newValue, t);
}
model.setTranslating(false);
}, Platform::runLater);
CompletableFuture.supplyAsync(() -> {
final var mainCollection = model.collections().get(model.videoLanguage());
try {
if (mainCollection == null) {
return translator.translate(model.selectedCollection(), value);
} else {
return translator.translate(mainCollection, value);
}
} catch (final TranslationException ex) {
throw new CompletionException(ex);
}
}).whenCompleteAsync((r, t) -> {
if (t == null) {
loadCollection(r);
model.setSelectedCollection(model.collections().get(value));
} else {
logger.error("Error while translating to {}", value, t);
}
model.setTranslating(false);
}, Platform::runLater);
}
});
binder.createBindings();
@@ -197,7 +210,7 @@ public class FXSubtitlesController extends AbstractFXController implements Subti
final var toRemove = new ArrayList<Tab>();
final var toAdd = new ArrayList<Tab>();
tabPane.getTabs().forEach(tab -> {
if (!model.collections().containsKey(Language.getLanguage(tab.getText()))) {
if (tab != mainSubtitlesTab && !model.collections().containsKey(Language.getLanguage(tab.getText()))) {
toRemove.add(tab);
}
});
@@ -237,7 +250,9 @@ public class FXSubtitlesController extends AbstractFXController implements Subti
filePicker.getExtensionFilters().addAll(archiveFilter, allSupportedFilter);
filePicker.setSelectedExtensionFilter(allSupportedFilter);
final var file = filePicker.showOpenDialog(window());
loadSubtitles(file.toPath());
if (file != null) {
loadSubtitles(file.toPath());
}
}
@FXML
@@ -283,10 +298,11 @@ public class FXSubtitlesController extends AbstractFXController implements Subti
public void loadSubtitles(final Path file) {
try {
final var map = importerExporter.importSubtitles(file);
map.values().forEach(this::loadCollection);
if (model.videoLanguage() == Language.AUTO) {
model.setVideoLanguage(map.keySet().stream().findFirst().orElse(Language.AUTO));
}
map.values().forEach(this::loadCollection);
model.setSelectedCollection(model.collections().get(map.keySet().iterator().next()));
} catch (final IOException | ParseException e) {
logger.error("Error loading subtitles {}", file, e);
showErrorDialog(resources.getString("subtitles.load.error.title"), MessageFormat.format(resources.getString("subtitles.load.error.label"), file));
@@ -316,7 +332,7 @@ public class FXSubtitlesController extends AbstractFXController implements Subti
}
@Override
protected Window window() {
public Window window() {
return saveButton.getScene().getWindow();
}
}

View File

@@ -10,6 +10,7 @@ import com.github.gtache.autosubtitle.gui.fx.FXSubtitlesController;
import com.github.gtache.autosubtitle.gui.fx.FXWorkController;
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
import com.github.gtache.autosubtitle.modules.setup.gui.fx.FXSetupModule;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
@@ -19,12 +20,11 @@ import javafx.scene.image.Image;
import javax.inject.Singleton;
import java.io.ByteArrayInputStream;
import java.util.ResourceBundle;
import java.util.prefs.Preferences;
/**
* Dagger module for FX
*/
@Module
@Module(includes = FXSetupModule.class)
public abstract class FXModule {
@Binds
@@ -69,10 +69,4 @@ public abstract class FXModule {
static Image providesPauseImage(@Pause final byte[] pauseImage) {
return new Image(new ByteArrayInputStream(pauseImage));
}
@Provides
@Singleton
static Preferences providesPreferences() {
return Preferences.userNodeForPackage(FXParametersController.class);
}
}

View File

@@ -0,0 +1,19 @@
package com.github.gtache.autosubtitle.modules.setup.gui.fx;
import com.github.gtache.autosubtitle.setup.SetupUserBridge;
import com.github.gtache.autosubtitle.setup.gui.fx.FXSetupUserBridge;
import dagger.Binds;
import dagger.Module;
/**
* Dagger module for FX setup
*/
@Module
public abstract class FXSetupModule {
private FXSetupModule() {
}
@Binds
abstract SetupUserBridge bindsSetupUserBridge(final FXSetupUserBridge setupUserBridge);
}

View File

@@ -0,0 +1,64 @@
package com.github.gtache.autosubtitle.setup.gui.fx;
import com.github.gtache.autosubtitle.gui.fx.FXMainController;
import com.github.gtache.autosubtitle.setup.SetupUserBridge;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ChoiceDialog;
import javafx.scene.control.TextInputDialog;
import javax.inject.Inject;
import java.util.List;
import java.util.Objects;
/**
* FX implementation of {@link SetupUserBridge}
*/
public class FXSetupUserBridge implements SetupUserBridge {
private final FXMainController controller;
@Inject
FXSetupUserBridge(final FXMainController mainController) {
this.controller = Objects.requireNonNull(mainController);
}
@Override
public boolean askForUserConfirmation(final String question) {
return showConfirmationDialog("Confirmation", question);
}
@Override
public <T> T askForUserChoice(final String question, final List<T> choices) {
return showChoiceDialog("Choice", question, choices);
}
@Override
public String askForUserInput(final String question) {
return showInputDialog("Input", question);
}
private <T> T showChoiceDialog(final String title, final String message, final List<T> choices) {
final var dialog = new ChoiceDialog<>(choices.getFirst(), choices);
dialog.initOwner(controller.window());
dialog.setHeaderText(message);
dialog.setTitle(title);
return dialog.showAndWait().orElse(null);
}
private String showInputDialog(final String title, final String message) {
final var dialog = new TextInputDialog();
dialog.initOwner(controller.window());
dialog.setHeaderText(message);
dialog.setTitle(title);
return dialog.showAndWait().orElse(null);
}
private boolean showConfirmationDialog(final String title, final String message) {
final var alert = new Alert(Alert.AlertType.CONFIRMATION, message, ButtonType.YES, ButtonType.NO);
alert.initOwner(controller.window());
alert.setHeaderText(null);
alert.setTitle(title);
return alert.showAndWait().map(bt -> bt == ButtonType.YES).orElse(false);
}
}

View File

@@ -13,7 +13,8 @@ module com.github.gtache.autosubtitle.gui.fx {
requires transitive java.prefs;
exports com.github.gtache.autosubtitle.gui.fx;
exports com.github.gtache.autosubtitle.modules.gui.fx;
exports com.github.gtache.autosubtitle.setup.gui.fx;
exports com.github.gtache.autosubtitle.subtitle.gui.fx;
exports com.github.gtache.autosubtitle.modules.gui.fx;
opens com.github.gtache.autosubtitle.gui.fx to javafx.fxml;
}

View File

@@ -4,37 +4,42 @@
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import org.controlsfx.control.PrefixSelectionComboBox?>
<GridPane hgap="10.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" vgap="10.0"
xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.github.gtache.autosubtitle.gui.fx.FXParametersController">
<GridPane hgap="10.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" vgap="10.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.github.gtache.autosubtitle.gui.fx.FXParametersController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES"/>
<ColumnConstraints hgrow="SOMETIMES"/>
<ColumnConstraints hgrow="SOMETIMES" />
<ColumnConstraints hgrow="SOMETIMES" />
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label text="%parameters.extraction.model.label"/>
<PrefixSelectionComboBox fx:id="extractionModelCombobox" GridPane.columnIndex="1"/>
<PrefixSelectionComboBox fx:id="extractionOutputFormat" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<Label text="%parameters.subtitles.output.format" GridPane.rowIndex="1"/>
<Label text="%parameters.subtitles.font.size" GridPane.rowIndex="3"/>
<Label text="%parameters.subtitles.font.family" GridPane.rowIndex="2"/>
<PrefixSelectionComboBox fx:id="fontFamilyCombobox" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<TextField fx:id="fontSizeField" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<Button mnemonicParsing="false" onAction="#savePressed" text="%parameters.button.save.label"
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<Button mnemonicParsing="false" onAction="#resetPressed" text="%parameters.button.reset.label"
GridPane.rowIndex="4"/>
<Label text="%parameters.extraction.model.label" />
<PrefixSelectionComboBox fx:id="extractionModelCombobox" GridPane.columnIndex="1" />
<PrefixSelectionComboBox fx:id="extractionOutputFormat" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<Label text="%parameters.subtitles.output.format" GridPane.rowIndex="1" />
<Label text="%parameters.subtitles.font.size" GridPane.rowIndex="3" />
<Label text="%parameters.subtitles.font.family" GridPane.rowIndex="2" />
<PrefixSelectionComboBox fx:id="fontFamilyCombobox" GridPane.columnIndex="1" GridPane.rowIndex="2" />
<TextField fx:id="fontSizeField" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<Button mnemonicParsing="false" onAction="#savePressed" text="%parameters.button.save.label" GridPane.columnIndex="1" GridPane.rowIndex="6" />
<Button mnemonicParsing="false" onAction="#resetPressed" text="%parameters.button.reset.label" GridPane.rowIndex="6" />
<Label text="%parameters.subtitles.max.length.label" GridPane.rowIndex="4" />
<Label text="%parameters.subtitles.max.lines.label" GridPane.rowIndex="5" />
<TextField fx:id="maxLengthField" GridPane.columnIndex="1" GridPane.rowIndex="4" />
<TextField fx:id="maxLinesField" GridPane.columnIndex="1" GridPane.rowIndex="5" />
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</GridPane>

View File

@@ -1,6 +1,5 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
@@ -8,6 +7,7 @@ import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.translation.Translator;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
@@ -51,7 +51,7 @@ class TestFXParametersController extends FxRobot {
this.videoConverter = requireNonNull(videoConverter);
this.translator = requireNonNull(translator);
this.timeFormatter = requireNonNull(timeFormatter);
this.model = spy(new FXParametersModel(mock(ExtractionModelProvider.class), "Arial", 12));
this.model = spy(new FXParametersModel(mock(ExtractionModelProvider.class), "Arial", 12, 40, 1));
this.binder = requireNonNull(binder);
this.window = FxToolkit.registerPrimaryStage();
this.controller = spy(FXParametersController.class);

View File

@@ -21,6 +21,8 @@ class TestFXParametersModel {
private static final String DEFAULT_FONT_FAMILY = "Arial";
private static final int DEFAULT_FONT_SIZE = 12;
private static final int DEFAULT_MAX_LINE_LENGTH = 40;
private static final int DEFAULT_MAX_LINES = 2;
private final List<ExtractionModel> availableExtractionModels;
private final ExtractionModel defaultExtractionModel;
private final ExtractionModelProvider provider;
@@ -34,7 +36,7 @@ class TestFXParametersModel {
when(provider.getDefaultExtractionModel()).thenReturn(defaultExtractionModel);
this.availableExtractionModels = List.of(defaultExtractionModel, extractionModel);
when(provider.getAvailableExtractionModels()).thenReturn(availableExtractionModels);
model = new FXParametersModel(extractionModelProvider, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
model = new FXParametersModel(extractionModelProvider, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DEFAULT_MAX_LINE_LENGTH, DEFAULT_MAX_LINES);
}
@Test

View File

@@ -1,12 +1,12 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.translation.Translator;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;

View File

@@ -1,12 +1,12 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.modules.gui.fx.DaggerResourceComponent;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.translation.Translator;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;

View File

@@ -12,7 +12,7 @@ class TestFXWorkBinder {
@Test
void testBindings() {
final var workModel = new FXWorkModel();
final var parametersModel = new FXParametersModel(mock(ExtractionModelProvider.class), "Arial", 12);
final var parametersModel = new FXParametersModel(mock(ExtractionModelProvider.class), "Arial", 12, 40, 1);
final var binder = new FXWorkBinder(workModel, parametersModel);
binder.createBindings();
assertNull(workModel.extractionModel());

View File

@@ -11,7 +11,6 @@ import javafx.scene.image.Image;
import org.junit.jupiter.api.Test;
import java.util.ResourceBundle;
import java.util.prefs.Preferences;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.Mockito.mock;
@@ -36,8 +35,4 @@ class TestFXModule {
assertInstanceOf(Image.class, FXModule.providesPauseImage(IMAGE));
}
@Test
void testPreferences() {
assertInstanceOf(Preferences.class, FXModule.providesPreferences());
}
}

View File

@@ -12,6 +12,10 @@
<artifactId>autosubtitle-gui-run</artifactId>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-deepl</artifactId>
</dependency>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-ffmpeg</artifactId>

View File

@@ -1,53 +0,0 @@
package com.github.gtache.autosubtitle.modules.gui.run;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.modules.setup.impl.TranslatorSetup;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import dagger.Module;
import dagger.Provides;
/**
* Module for missing components
*/
@Module
public abstract class MissingComponentsModule {
private MissingComponentsModule() {
}
@Provides
static Translator providesTranslator() {
return new Translator<>() {
@Override
public Language getLanguage(final String text) {
return Language.getDefault();
}
@Override
public String translate(final String text, final Language to) {
return text;
}
@Override
public Subtitle translate(final Subtitle subtitle, final Language to) {
return subtitle;
}
@Override
public SubtitleCollection<Subtitle> translate(final SubtitleCollection<?> collection, final Language to) {
return (SubtitleCollection<Subtitle>) collection;
}
};
}
@Provides
@TranslatorSetup
static SetupManager providesTranslatorSetupManager() {
return new NoOpSetupManager();
}
}

View File

@@ -1,64 +0,0 @@
package com.github.gtache.autosubtitle.modules.gui.run;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupListener;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.SetupStatus;
class NoOpSetupManager implements SetupManager {
@Override
public String name() {
return "NoOpSetupManager";
}
@Override
public SetupStatus status() {
return SetupStatus.NOT_INSTALLED;
}
@Override
public boolean isInstalled() throws SetupException {
return false;
}
@Override
public void install() throws SetupException {
}
@Override
public void uninstall() throws SetupException {
}
@Override
public void reinstall() throws SetupException {
}
@Override
public boolean isUpdateAvailable() throws SetupException {
return false;
}
@Override
public void update() throws SetupException {
}
@Override
public void addListener(final SetupListener listener) {
}
@Override
public void removeListener(final SetupListener listener) {
}
@Override
public void removeListeners() {
}
}

View File

@@ -1,5 +1,6 @@
package com.github.gtache.autosubtitle.modules.gui.run;
import com.github.gtache.autosubtitle.modules.deepl.DeepLModule;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegModule;
import com.github.gtache.autosubtitle.modules.gui.fx.FXModule;
import com.github.gtache.autosubtitle.modules.gui.impl.GuiCoreModule;
@@ -15,7 +16,7 @@ import javax.inject.Singleton;
*/
@Singleton
@Component(modules = {CoreModule.class, GuiCoreModule.class, FXModule.class, FFmpegModule.class,
WhisperXModule.class, MissingComponentsModule.class})
WhisperXModule.class, DeepLModule.class})
public interface RunComponent {
/**

View File

@@ -1,7 +1,8 @@
/**
* Main module for auto-subtitle
*/
module com.github.gtache.autosubtitle.run {
module com.github.gtache.autosubtitle.gui.run {
requires com.github.gtache.autosubtitle.deepl;
requires com.github.gtache.autosubtitle.ffmpeg;
requires com.github.gtache.autosubtitle.gui.fx;
requires com.github.gtache.autosubtitle.whisperx;

View File

@@ -48,8 +48,8 @@ public class JSONSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
try {
final var json = gson.fromJson(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;
final var start = (long) (s.start() * 1000L);
final var end = (long) (s.end() * 1000L);
return new SubtitleImpl(s.text(), start, end, null, null);
}).sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList();
final var language = Language.getLanguage(json.language());

View File

@@ -55,10 +55,8 @@ public class WhisperXSubtitleExtractor extends AbstractWhisperSubtitleExtractor
args.add("True");
args.add("--compute_type");
args.add("int8");
args.add("--max_line_count");
args.add("2");
args.add("--max_line_width");
args.add("30");
args.add("--threads");
args.add(String.valueOf(Runtime.getRuntime().availableProcessors()));
if (language != Language.AUTO) {
args.add("--language");
args.add(language.iso2());

View File

@@ -1,6 +1,8 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisperx;
import com.github.gtache.autosubtitle.Language;
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.ParseException;
@@ -12,10 +14,14 @@ import com.google.gson.Gson;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Path;
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;
/**
* {@link SubtitleConverter} implementation for JSON files
@@ -23,11 +29,21 @@ import java.util.stream.Collectors;
@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;
@Inject
JSONSubtitleConverter(final Gson gson) {
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;
}
@Override
@@ -42,10 +58,15 @@ public class JSONSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
public SubtitleCollectionImpl<SubtitleImpl> parse(final String content) throws ParseException {
try {
final var json = gson.fromJson(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;
return new SubtitleImpl(s.text(), start, end, null, null);
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);
}
}).sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList();
final var language = Language.getLanguage(json.language());
final var subtitlesText = subtitles.stream().map(Subtitle::content).collect(Collectors.joining(""));
@@ -55,6 +76,79 @@ 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);
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);
} else {
return splitSubtitleWords(segment);
}
}
private Stream<SubtitleImpl> splitSubtitleLines(final JSONSubtitleSegment segment) {
final var maxLineLength = preferences.getInt(MAX_LINE_LENGTH, defaultMaxLineLength);
final var text = segment.text();
final var split = SPLIT_PATTERN.split(text);
final var builder = new StringBuilder(text.length());
for (final var s : split) {
final var newLength = builder.length() + s.length();
if (areDifferentLines(builder.length(), newLength, maxLineLength)) {
builder.append("\n").append(s);
} else {
builder.append(" ").append(s);
}
}
final var start = (long) (segment.start() * 1000L);
final var end = (long) (segment.end() * 1000L);
return Stream.of(new SubtitleImpl(builder.toString(), start, end, null, null));
}
private static boolean areDifferentLines(final int currentLength, final int newLength, final int maxLength) {
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);
final var ret = new ArrayList<SubtitleImpl>(segment.text().length() / (maxLines * maxLineLength));
final var builder = new StringBuilder(maxLines * maxLineLength);
final var words = segment.words();
var currentStart = -1L;
var currentEnd = -1L;
for (final var word : words) {
final var text = word.word();
final var start = (long) (word.start() * 1000L);
final var end = (long) (word.end() * 1000L);
if (currentStart < 0) {
currentStart = start;
}
final var newLength = builder.length() + text.length();
if (newLength > maxLineLength * maxLines) {
final var newSubtitle = new SubtitleImpl(builder.toString(), currentStart, currentEnd, null, null);
ret.add(newSubtitle);
builder.delete(0, builder.length());
builder.append(text);
currentStart = start == 0 ? currentEnd : start;
} else if (areDifferentLines(builder.length(), newLength, maxLineLength)) {
builder.append("\n").append(text);
} else {
builder.append(" ").append(text);
}
currentEnd = end == 0 ? currentEnd : end;
}
if (!builder.isEmpty()) {
final var newSubtitle = new SubtitleImpl(builder.toString(), currentStart, currentEnd, null, null);
ret.add(newSubtitle);
}
return ret.stream();
}
@Override
public boolean canParse(final Path file) {
return file.getFileName().toString().endsWith(".json");

View File

@@ -4,6 +4,7 @@
module com.github.gtache.autosubtitle.whisperx {
requires transitive com.github.gtache.autosubtitle.whisper.common;
requires org.apache.logging.log4j;
requires java.prefs;
exports com.github.gtache.autosubtitle.whisperx;
exports com.github.gtache.autosubtitle.setup.whisperx;