Pipeline working, implements FFmpegSetupManager
This commit is contained in:
6
.idea/scala_compiler.xml
generated
Normal file
6
.idea/scala_compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ScalaCompilerConfiguration">
|
||||||
|
<option name="separateProdTestSources" value="false" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
.idea/sonarlint.xml
generated
Normal file
10
.idea/sonarlint.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SonarLintProjectSettings">
|
||||||
|
<option name="moduleMapping">
|
||||||
|
<map>
|
||||||
|
<entry key="autosubtitle-gui-fx" value="autosubtitle-fx" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -3,13 +3,18 @@ package com.github.gtache.autosubtitle;
|
|||||||
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
|
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
public interface VideoConverter {
|
public interface VideoConverter {
|
||||||
|
|
||||||
Video addSoftSubtitles(final Video video, final Collection<SubtitleCollection> subtitles) throws IOException;
|
Video addSoftSubtitles(final Video video, final Collection<SubtitleCollection> subtitles) throws IOException;
|
||||||
|
|
||||||
|
void addSoftSubtitles(final Video video, final Collection<SubtitleCollection> subtitles, final Path path) throws IOException;
|
||||||
|
|
||||||
Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException;
|
Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException;
|
||||||
|
|
||||||
|
void addHardSubtitles(final Video video, final SubtitleCollection subtitles, final Path path) throws IOException;
|
||||||
|
|
||||||
Audio getAudio(final Video video) throws IOException;
|
Audio getAudio(final Video video) throws IOException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ public interface Subtitle {
|
|||||||
return Duration.ofMillis(end() - start());
|
return Duration.ofMillis(end() - start());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the subtitle is shown at the given time
|
||||||
|
*
|
||||||
|
* @param time the time
|
||||||
|
* @return true if the subtitle is shown
|
||||||
|
*/
|
||||||
|
default boolean isShowing(final long time) {
|
||||||
|
return time >= start() && time <= end();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the font of the subtitle
|
* @return the font of the subtitle
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ import com.github.gtache.autosubtitle.modules.deepl.DeepLModule;
|
|||||||
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegModule;
|
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegModule;
|
||||||
import com.github.gtache.autosubtitle.modules.impl.CoreModule;
|
import com.github.gtache.autosubtitle.modules.impl.CoreModule;
|
||||||
import com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper.WhisperExtractorModule;
|
import com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper.WhisperExtractorModule;
|
||||||
import com.github.gtache.autosubtitle.modules.subtitle.impl.SubtitleModule;
|
|
||||||
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
|
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
|
||||||
import dagger.Component;
|
import dagger.Component;
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Component(modules = {SubtitleModule.class, CoreModule.class, DeepLModule.class, FFmpegModule.class, WhisperExtractorModule.class})
|
@Component(modules = {CoreModule.class, DeepLModule.class, FFmpegModule.class, WhisperExtractorModule.class})
|
||||||
@Singleton
|
@Singleton
|
||||||
public interface CliComponent {
|
public interface CliComponent {
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public enum Architecture {
|
|||||||
//64 bit
|
//64 bit
|
||||||
X86, X86_64, AMD64,
|
X86, X86_64, AMD64,
|
||||||
//ARM 32 bit
|
//ARM 32 bit
|
||||||
ARM32, ARM, ARMV1, ARMV2, ARMV3, ARMV4, ARMV5, ARMV6, ARMV7, AARCH32,
|
ARM32, ARM, ARMV1, ARMV2, ARMV3, ARMV4, ARMV5, ARMV6, ARMV7, AARCH32, ARMHF, ARMEL,
|
||||||
//ARM 64 bit
|
//ARM 64 bit
|
||||||
ARM64, ARMV8, ARMV9, AARCH64,
|
ARM64, ARMV8, ARMV9, AARCH64,
|
||||||
UNKNOWN;
|
UNKNOWN;
|
||||||
|
|||||||
@@ -21,9 +21,4 @@ public record VideoInfoImpl(String videoFormat, int width, int height, long dura
|
|||||||
throw new IllegalArgumentException("Duration must be greater than 0 : " + duration);
|
throw new IllegalArgumentException("Duration must be greater than 0 : " + duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String videoFormat() {
|
|
||||||
return videoFormat;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,22 @@ package com.github.gtache.autosubtitle.modules.impl;
|
|||||||
|
|
||||||
import com.github.gtache.autosubtitle.impl.Architecture;
|
import com.github.gtache.autosubtitle.impl.Architecture;
|
||||||
import com.github.gtache.autosubtitle.impl.OS;
|
import com.github.gtache.autosubtitle.impl.OS;
|
||||||
|
import com.github.gtache.autosubtitle.modules.setup.impl.SetupModule;
|
||||||
|
import com.github.gtache.autosubtitle.modules.subtitle.impl.SubtitleModule;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
/**
|
||||||
|
* Dagger module for Core
|
||||||
|
*/
|
||||||
|
@Module(includes = {SetupModule.class, SubtitleModule.class})
|
||||||
|
public final class CoreModule {
|
||||||
|
|
||||||
@Module
|
private CoreModule() {
|
||||||
public abstract class CoreModule {
|
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
static OS providesOS() {
|
static OS providesOS() {
|
||||||
final var name = System.getProperty("os.name");
|
final var name = System.getProperty("os.name");
|
||||||
if (name.contains("Windows")) {
|
if (name.contains("Windows")) {
|
||||||
@@ -24,14 +30,12 @@ public abstract class CoreModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
static Architecture providesArchitecture() {
|
static Architecture providesArchitecture() {
|
||||||
final var arch = System.getProperty("os.arch");
|
final var arch = System.getProperty("os.arch");
|
||||||
return Architecture.getArchitecture(arch);
|
return Architecture.getArchitecture(arch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@ExecutableExtension
|
@ExecutableExtension
|
||||||
static String providesExecutableExtension(final OS os) {
|
static String providesExecutableExtension(final OS os) {
|
||||||
return os == OS.WINDOWS ? ".exe" : "";
|
return os == OS.WINDOWS ? ".exe" : "";
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.github.gtache.autosubtitle.modules.setup.impl;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dagger core module for setup
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
public final class SetupModule {
|
||||||
|
|
||||||
|
private SetupModule() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static HttpClient providesHttpClient() {
|
||||||
|
return HttpClient.newHttpClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,13 @@ import dagger.multibindings.StringKey;
|
|||||||
* Dagger module for subtitles
|
* Dagger module for subtitles
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
public interface SubtitleModule {
|
public abstract class SubtitleModule {
|
||||||
|
|
||||||
|
private SubtitleModule() {
|
||||||
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@StringKey("srt")
|
@StringKey("srt")
|
||||||
SubtitleConverter bindsSubtitleConverter(final SRTSubtitleConverter converter);
|
abstract SubtitleConverter bindsSubtitleConverter(final SRTSubtitleConverter converter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public abstract class AbstractProcessRunner implements ProcessRunner {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Process start(final List<String> args) throws IOException {
|
public Process start(final List<String> args) throws IOException {
|
||||||
|
logger.info("Running {}", args);
|
||||||
final var builder = new ProcessBuilder(args);
|
final var builder = new ProcessBuilder(args);
|
||||||
builder.redirectErrorStream(true);
|
builder.redirectErrorStream(true);
|
||||||
return builder.start();
|
return builder.start();
|
||||||
@@ -46,4 +47,22 @@ public abstract class AbstractProcessRunner implements ProcessRunner {
|
|||||||
final var process = start(args);
|
final var process = start(args);
|
||||||
return new ProcessListenerImpl(process);
|
return new ProcessListenerImpl(process);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a process and writes the output to the log
|
||||||
|
*
|
||||||
|
* @param args the command
|
||||||
|
* @return the result
|
||||||
|
* @throws IOException if an error occurs
|
||||||
|
*/
|
||||||
|
protected ProcessResult runListen(final List<String> args) throws IOException {
|
||||||
|
final var listener = startListen(args);
|
||||||
|
var line = listener.readLine();
|
||||||
|
final var processName = args.getFirst();
|
||||||
|
while (line != null) {
|
||||||
|
logger.info("[{}]: {}", processName, line);
|
||||||
|
line = listener.readLine();
|
||||||
|
}
|
||||||
|
return listener.join(Duration.ofHours(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.github.gtache.autosubtitle.process.impl;
|
|||||||
import com.github.gtache.autosubtitle.process.ProcessListener;
|
import com.github.gtache.autosubtitle.process.ProcessListener;
|
||||||
import com.github.gtache.autosubtitle.process.ProcessResult;
|
import com.github.gtache.autosubtitle.process.ProcessResult;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
@@ -30,7 +29,7 @@ public class ProcessListenerImpl implements ProcessListener {
|
|||||||
*/
|
*/
|
||||||
public ProcessListenerImpl(final Process process) {
|
public ProcessListenerImpl(final Process process) {
|
||||||
this.process = Objects.requireNonNull(process);
|
this.process = Objects.requireNonNull(process);
|
||||||
this.reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(process.getInputStream()), StandardCharsets.UTF_8));
|
this.reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
|
||||||
this.output = new ArrayList<>();
|
this.output = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -19,6 +23,8 @@ import java.util.HashSet;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all {@link SetupManager} implementations
|
* Base class for all {@link SetupManager} implementations
|
||||||
*/
|
*/
|
||||||
@@ -26,12 +32,23 @@ public abstract class AbstractSetupManager extends AbstractProcessRunner impleme
|
|||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(AbstractSetupManager.class);
|
private static final Logger logger = LogManager.getLogger(AbstractSetupManager.class);
|
||||||
private final Set<SetupListener> listeners;
|
private final Set<SetupListener> listeners;
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates the manager
|
* Instantiates the manager with a default client
|
||||||
*/
|
*/
|
||||||
protected AbstractSetupManager() {
|
protected AbstractSetupManager() {
|
||||||
|
this(HttpClient.newHttpClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the manager with the given client
|
||||||
|
*
|
||||||
|
* @param httpClient The HTTP client to use
|
||||||
|
*/
|
||||||
|
protected AbstractSetupManager(final HttpClient httpClient) {
|
||||||
this.listeners = new HashSet<>();
|
this.listeners = new HashSet<>();
|
||||||
|
this.httpClient = requireNonNull(httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -158,6 +175,30 @@ public abstract class AbstractSetupManager extends AbstractProcessRunner impleme
|
|||||||
logger.info("{} deleted", path);
|
logger.info("{} deleted", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a file
|
||||||
|
*
|
||||||
|
* @param url The file url
|
||||||
|
* @param path The save path
|
||||||
|
* @throws SetupException If an error occurs
|
||||||
|
*/
|
||||||
|
protected void download(final String url, final Path path) throws SetupException {
|
||||||
|
final var request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
|
||||||
|
try {
|
||||||
|
final var result = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(path));
|
||||||
|
if (result.statusCode() == 200) {
|
||||||
|
logger.info("{} download successful", path);
|
||||||
|
} else {
|
||||||
|
throw new SetupException("Error downloading " + path + ": " + result.body());
|
||||||
|
}
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new SetupException(e);
|
||||||
|
} catch (final InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new SetupException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static long getFilesCount(final Path path) throws IOException {
|
private static long getFilesCount(final Path path) throws IOException {
|
||||||
try (final var stream = Files.walk(path)) {
|
try (final var stream = Files.walk(path)) {
|
||||||
return stream.count();
|
return stream.count();
|
||||||
|
|||||||
@@ -35,11 +35,21 @@ public class SRTSubtitleConverter implements SubtitleConverter {
|
|||||||
return IntStream.range(0, subtitles.size()).mapToObj(i -> {
|
return IntStream.range(0, subtitles.size()).mapToObj(i -> {
|
||||||
final var subtitle = subtitles.get(i);
|
final var subtitle = subtitles.get(i);
|
||||||
return (i + 1) + "\n" +
|
return (i + 1) + "\n" +
|
||||||
subtitle.start() + " --> " + subtitle.end() + "\n" +
|
formatTime(subtitle.start()) + " --> " + formatTime(subtitle.end()) + "\n" +
|
||||||
subtitle.content();
|
subtitle.content();
|
||||||
}).collect(Collectors.joining("\n\n"));
|
}).collect(Collectors.joining("\n\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String formatTime(final long time) {
|
||||||
|
final var millisPerHour = 3600000;
|
||||||
|
final var millisPerMinute = 60000;
|
||||||
|
final var hours = time / millisPerHour;
|
||||||
|
final var minutes = (time - hours * millisPerHour) / millisPerMinute;
|
||||||
|
final var seconds = (time - hours * millisPerHour - minutes * millisPerMinute) / 1000;
|
||||||
|
final var millis = time - hours * millisPerHour - minutes * millisPerMinute - seconds * 1000;
|
||||||
|
return String.format("%02d:%02d:%02d,%03d", hours, minutes, seconds, millis);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SubtitleCollection parse(final String content) throws ParseException {
|
public SubtitleCollection parse(final String content) throws ParseException {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
module com.github.gtache.autosubtitle.core {
|
module com.github.gtache.autosubtitle.core {
|
||||||
requires transitive com.github.gtache.autosubtitle.api;
|
requires transitive com.github.gtache.autosubtitle.api;
|
||||||
requires transitive dagger;
|
requires transitive dagger;
|
||||||
|
requires transitive java.net.http;
|
||||||
requires transitive javax.inject;
|
requires transitive javax.inject;
|
||||||
requires org.apache.logging.log4j;
|
requires org.apache.logging.log4j;
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,14 @@
|
|||||||
<groupId>com.github.gtache.autosubtitle</groupId>
|
<groupId>com.github.gtache.autosubtitle</groupId>
|
||||||
<artifactId>autosubtitle-core</artifactId>
|
<artifactId>autosubtitle-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-compress</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.tukaani</groupId>
|
||||||
|
<artifactId>xz</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@@ -49,19 +49,24 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Video addSoftSubtitles(final Video video, final Collection<SubtitleCollection> subtitles) throws IOException {
|
public Video addSoftSubtitles(final Video video, final Collection<SubtitleCollection> subtitles) throws IOException {
|
||||||
|
final var out = getTempFile("mkv"); //Soft ass subtitles are only supported by mkv apparently
|
||||||
|
addSoftSubtitles(video, subtitles, out);
|
||||||
|
return new FileVideoImpl(out, new VideoInfoImpl("mkv", video.info().width(), video.info().height(), video.info().duration()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addSoftSubtitles(final Video video, final Collection<SubtitleCollection> subtitles, final Path path) throws IOException {
|
||||||
final var videoPath = getPath(video);
|
final var videoPath = getPath(video);
|
||||||
final var collectionMap = dumpCollections(subtitles);
|
final var collectionMap = dumpCollections(subtitles);
|
||||||
final var out = getTempFile("mkv"); //Soft subtitles are only supported by mkv apparently
|
|
||||||
final var args = new ArrayList<String>();
|
final var args = new ArrayList<String>();
|
||||||
args.add(getFFmpegPath());
|
args.add(getFFmpegPath());
|
||||||
|
args.add("-y");
|
||||||
args.add("-i");
|
args.add("-i");
|
||||||
args.add(videoPath.toString());
|
args.add(videoPath.toString());
|
||||||
collectionMap.forEach((c, p) -> {
|
collectionMap.forEach((c, p) -> {
|
||||||
args.add("-i");
|
args.add("-i");
|
||||||
args.add(p.toString());
|
args.add(p.toString());
|
||||||
});
|
});
|
||||||
args.add("-c");
|
|
||||||
args.add("copy");
|
|
||||||
args.add("-map");
|
args.add("-map");
|
||||||
args.add("0:v");
|
args.add("0:v");
|
||||||
args.add("-map");
|
args.add("-map");
|
||||||
@@ -71,30 +76,56 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video
|
|||||||
final var n = i.getAndIncrement();
|
final var n = i.getAndIncrement();
|
||||||
args.add("-map");
|
args.add("-map");
|
||||||
args.add(String.valueOf(n));
|
args.add(String.valueOf(n));
|
||||||
|
});
|
||||||
|
args.add("-c:v");
|
||||||
|
args.add("copy");
|
||||||
|
args.add("-c:a");
|
||||||
|
args.add("copy");
|
||||||
|
final var extension = path.toString().substring(path.toString().lastIndexOf('.') + 1);
|
||||||
|
if (extension.equals("mp4")) {
|
||||||
|
args.add("-c:s");
|
||||||
|
args.add("mov_text");
|
||||||
|
} else {
|
||||||
|
args.add("-c:s");
|
||||||
|
args.add(subtitleConverter.formatName());
|
||||||
|
}
|
||||||
|
final var j = new AtomicInteger(0);
|
||||||
|
collectionMap.forEach((c, p) -> {
|
||||||
|
final var n = j.getAndIncrement();
|
||||||
args.add("-metadata:s:s:" + n);
|
args.add("-metadata:s:s:" + n);
|
||||||
args.add("language=" + c.language().iso3());
|
args.add("language=" + c.language().iso3());
|
||||||
});
|
});
|
||||||
args.add(out.toString());
|
args.add(path.toString());
|
||||||
run(args);
|
runListen(args);
|
||||||
return new FileVideoImpl(out, new VideoInfoImpl("mkv", video.info().width(), video.info().height(), video.info().duration()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException {
|
public Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException {
|
||||||
|
final var out = getTempFile(video.info().videoFormat());
|
||||||
|
addHardSubtitles(video, subtitles, out);
|
||||||
|
return new FileVideoImpl(out, video.info());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addHardSubtitles(final Video video, final SubtitleCollection subtitles, final Path path) throws IOException {
|
||||||
final var videoPath = getPath(video);
|
final var videoPath = getPath(video);
|
||||||
final var subtitlesPath = dumpSubtitles(subtitles);
|
final var subtitlesPath = dumpSubtitles(subtitles);
|
||||||
final var out = getTempFile(video.info().videoFormat());
|
final var escapedPath = escapeVF(subtitlesPath.toString());
|
||||||
final var subtitleArg = subtitleConverter.formatName().equalsIgnoreCase("ass") ? "ass=" + subtitlesPath : "subtitles=" + subtitlesPath;
|
final var subtitleArg = subtitleConverter.formatName().equalsIgnoreCase("ass") ? "ass='" + escapedPath + "'" : "subtitles='" + escapedPath + "'";
|
||||||
final var args = List.of(
|
final var args = List.of(
|
||||||
getFFmpegPath(),
|
getFFmpegPath(),
|
||||||
"-i",
|
"-i",
|
||||||
videoPath.toString(),
|
videoPath.toString(),
|
||||||
"-vf",
|
"-vf",
|
||||||
subtitleArg,
|
subtitleArg,
|
||||||
out.toString()
|
path.toString()
|
||||||
);
|
);
|
||||||
run(args);
|
runListen(args);
|
||||||
return new FileVideoImpl(out, video.info());
|
}
|
||||||
|
|
||||||
|
private static String escapeVF(final String path) {
|
||||||
|
return path.replace("\\", "\\\\").replace(":", "\\:").replace("'", "'\\''")
|
||||||
|
.replace("%", "\\%");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -104,6 +135,7 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video
|
|||||||
final var dumpVideoPath = getTempFile("." + video.info().videoFormat());
|
final var dumpVideoPath = getTempFile("." + video.info().videoFormat());
|
||||||
final var args = List.of(
|
final var args = List.of(
|
||||||
getFFmpegPath(),
|
getFFmpegPath(),
|
||||||
|
"-y",
|
||||||
"-i",
|
"-i",
|
||||||
videoPath.toString(),
|
videoPath.toString(),
|
||||||
"-map",
|
"-map",
|
||||||
@@ -113,7 +145,7 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video
|
|||||||
"0:v",
|
"0:v",
|
||||||
dumpVideoPath.toString()
|
dumpVideoPath.toString()
|
||||||
);
|
);
|
||||||
run(args);
|
runListen(args);
|
||||||
Files.deleteIfExists(dumpVideoPath);
|
Files.deleteIfExists(dumpVideoPath);
|
||||||
return new FileAudioImpl(audioPath, new AudioInfoImpl("wav", video.info().duration()));
|
return new FileAudioImpl(audioPath, new AudioInfoImpl("wav", video.info().duration()));
|
||||||
}
|
}
|
||||||
@@ -143,7 +175,7 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Path dumpSubtitles(final SubtitleCollection subtitles) throws IOException {
|
private Path dumpSubtitles(final SubtitleCollection subtitles) throws IOException {
|
||||||
final var path = getTempFile("ass");
|
final var path = getTempFile("srt");
|
||||||
Files.writeString(path, subtitleConverter.format(subtitles));
|
Files.writeString(path, subtitleConverter.format(subtitles));
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.github.gtache.autosubtitle.modules.ffmpeg;
|
||||||
|
|
||||||
|
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 FFProbeInstallerPath {
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.github.gtache.autosubtitle.modules.ffmpeg;
|
||||||
|
|
||||||
|
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 FFmpegInstallerPath {
|
||||||
|
}
|
||||||
@@ -4,15 +4,23 @@ import com.github.gtache.autosubtitle.VideoConverter;
|
|||||||
import com.github.gtache.autosubtitle.VideoLoader;
|
import com.github.gtache.autosubtitle.VideoLoader;
|
||||||
import com.github.gtache.autosubtitle.ffmpeg.FFmpegVideoConverter;
|
import com.github.gtache.autosubtitle.ffmpeg.FFmpegVideoConverter;
|
||||||
import com.github.gtache.autosubtitle.ffmpeg.FFprobeVideoLoader;
|
import com.github.gtache.autosubtitle.ffmpeg.FFprobeVideoLoader;
|
||||||
|
import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFmpegSetupModule;
|
||||||
import dagger.Binds;
|
import dagger.Binds;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
|
|
||||||
@Module
|
/**
|
||||||
public interface FFmpegModule {
|
* Dagger module for FFmpeg
|
||||||
|
*/
|
||||||
|
@Module(includes = FFmpegSetupModule.class)
|
||||||
|
public abstract class FFmpegModule {
|
||||||
|
|
||||||
|
private FFmpegModule() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
VideoConverter bindsVideoConverter(final FFmpegVideoConverter converter);
|
abstract VideoConverter bindsVideoConverter(final FFmpegVideoConverter converter);
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
VideoLoader bindsVideoLoader(final FFprobeVideoLoader loader);
|
abstract VideoLoader bindsVideoLoader(final FFprobeVideoLoader loader);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.github.gtache.autosubtitle.modules.setup.ffmpeg;
|
package com.github.gtache.autosubtitle.modules.setup.ffmpeg;
|
||||||
|
|
||||||
|
import com.github.gtache.autosubtitle.impl.Architecture;
|
||||||
|
import com.github.gtache.autosubtitle.impl.OS;
|
||||||
import com.github.gtache.autosubtitle.modules.ffmpeg.FFBundledRoot;
|
import com.github.gtache.autosubtitle.modules.ffmpeg.FFBundledRoot;
|
||||||
|
import com.github.gtache.autosubtitle.modules.ffmpeg.FFProbeInstallerPath;
|
||||||
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath;
|
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath;
|
||||||
|
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegInstallerPath;
|
||||||
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath;
|
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath;
|
||||||
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegVersion;
|
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegVersion;
|
||||||
import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeBundledPath;
|
import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeBundledPath;
|
||||||
@@ -9,12 +13,18 @@ import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeSystemPath;
|
|||||||
import com.github.gtache.autosubtitle.modules.impl.ExecutableExtension;
|
import com.github.gtache.autosubtitle.modules.impl.ExecutableExtension;
|
||||||
import com.github.gtache.autosubtitle.modules.setup.impl.VideoConverterSetup;
|
import com.github.gtache.autosubtitle.modules.setup.impl.VideoConverterSetup;
|
||||||
import com.github.gtache.autosubtitle.setup.SetupManager;
|
import com.github.gtache.autosubtitle.setup.SetupManager;
|
||||||
|
import com.github.gtache.autosubtitle.setup.ffmpeg.Decompresser;
|
||||||
|
import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupConfiguration;
|
||||||
import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupManager;
|
import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupManager;
|
||||||
|
import com.github.gtache.autosubtitle.setup.ffmpeg.TarDecompresser;
|
||||||
|
import com.github.gtache.autosubtitle.setup.ffmpeg.XZDecompresser;
|
||||||
|
import com.github.gtache.autosubtitle.setup.ffmpeg.ZipDecompresser;
|
||||||
import dagger.Binds;
|
import dagger.Binds;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
import dagger.multibindings.StringKey;
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
@@ -27,47 +37,87 @@ public abstract class FFmpegSetupModule {
|
|||||||
private static final String FFMPEG = "ffmpeg";
|
private static final String FFMPEG = "ffmpeg";
|
||||||
private static final String FFPROBE = "ffprobe";
|
private static final String FFPROBE = "ffprobe";
|
||||||
|
|
||||||
|
private FFmpegSetupModule() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@StringKey("zip")
|
||||||
|
@IntoMap
|
||||||
|
abstract Decompresser bindsZipDecompresser(final ZipDecompresser decompresser);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@StringKey("tar")
|
||||||
|
@IntoMap
|
||||||
|
abstract Decompresser bindsTarDecompresser(final TarDecompresser decompresser);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@StringKey("xz")
|
||||||
|
@IntoMap
|
||||||
|
abstract Decompresser bindsXzDecompresser(final XZDecompresser decompresser);
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@VideoConverterSetup
|
@VideoConverterSetup
|
||||||
abstract SetupManager bindsFFmpegSetupManager(final FFmpegSetupManager manager);
|
abstract SetupManager bindsFFmpegSetupManager(final FFmpegSetupManager manager);
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
static FFmpegSetupConfiguration providesFFmpegSetupConfiguration(@FFBundledRoot final Path root, @FFmpegBundledPath final Path bundledPath, @FFmpegSystemPath final Path systemPath,
|
||||||
|
@FFmpegInstallerPath final Path ffmpegInstallerPath, @FFProbeInstallerPath final Path ffprobeInstallerPath,
|
||||||
|
final OS os, final Architecture architecture) {
|
||||||
|
return new FFmpegSetupConfiguration(root, bundledPath, systemPath, ffmpegInstallerPath, ffprobeInstallerPath, os, architecture);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FFmpegInstallerPath
|
||||||
|
static Path providesFFmpegInstallerPath(@FFBundledRoot final Path root, final OS os) {
|
||||||
|
return root.resolve("cache").resolve("ffmpeg-installer" + getInstallerExtension(os));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FFProbeInstallerPath
|
||||||
|
static Path providesFFProbeInstallerPath(@FFBundledRoot final Path root, final OS os) {
|
||||||
|
return root.resolve("cache").resolve("ffprobe-installer" + getInstallerExtension(os));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getInstallerExtension(final OS os) {
|
||||||
|
if (os == OS.LINUX) {
|
||||||
|
return ".tar.gz";
|
||||||
|
} else {
|
||||||
|
return ".zip";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
@FFBundledRoot
|
@FFBundledRoot
|
||||||
static Path providesFFBundledRoot() {
|
static Path providesFFBundledRoot() {
|
||||||
return Paths.get("tools", FFMPEG);
|
return Paths.get("tools", FFMPEG);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@FFprobeBundledPath
|
@FFprobeBundledPath
|
||||||
static Path providesFFProbeBundledPath(@FFBundledRoot final Path root, @ExecutableExtension final String extension) {
|
static Path providesFFProbeBundledPath(@FFBundledRoot final Path root, @ExecutableExtension final String extension) {
|
||||||
return root.resolve(FFPROBE + extension);
|
return root.resolve(FFPROBE + extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@FFprobeSystemPath
|
@FFprobeSystemPath
|
||||||
static Path providesFFProbeSystemPath(@ExecutableExtension final String extension) {
|
static Path providesFFProbeSystemPath(@ExecutableExtension final String extension) {
|
||||||
return Paths.get(FFPROBE + extension);
|
return Paths.get(FFPROBE + extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@FFmpegBundledPath
|
@FFmpegBundledPath
|
||||||
static Path providesFFmpegBundledPath(@FFBundledRoot final Path root, @ExecutableExtension final String extension) {
|
static Path providesFFmpegBundledPath(@FFBundledRoot final Path root, @ExecutableExtension final String extension) {
|
||||||
return root.resolve(FFMPEG + extension);
|
return root.resolve(FFMPEG + extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@FFmpegSystemPath
|
@FFmpegSystemPath
|
||||||
static Path providesFFmpegSystemPath(@ExecutableExtension final String extension) {
|
static Path providesFFmpegSystemPath(@ExecutableExtension final String extension) {
|
||||||
return Paths.get(FFMPEG + extension);
|
return Paths.get(FFMPEG + extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@FFmpegVersion
|
@FFmpegVersion
|
||||||
static String providesFFmpegVersion() {
|
static String providesFFmpegVersion() {
|
||||||
return "7.0.1";
|
return "7.0.1";
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.github.gtache.autosubtitle.setup.ffmpeg;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unzips files
|
||||||
|
*/
|
||||||
|
public interface Decompresser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unzips an archive to the given destination
|
||||||
|
*
|
||||||
|
* @param archive The archive
|
||||||
|
* @param destination The destination folder
|
||||||
|
* @throws IOException if an error occurs
|
||||||
|
*/
|
||||||
|
void decompress(final Path archive, final Path destination) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given file is supported by the decompresser
|
||||||
|
*
|
||||||
|
* @param path The file path
|
||||||
|
* @return True if the file is supported
|
||||||
|
*/
|
||||||
|
boolean isPathSupported(final Path path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.github.gtache.autosubtitle.setup.ffmpeg;
|
||||||
|
|
||||||
|
import com.github.gtache.autosubtitle.impl.Architecture;
|
||||||
|
import com.github.gtache.autosubtitle.impl.OS;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for FFmpeg setup
|
||||||
|
*/
|
||||||
|
public record FFmpegSetupConfiguration(Path root, Path bundledFFmpegPath, Path systemFFmpegPath,
|
||||||
|
Path ffmpegInstallerPath, Path ffprobeInstallerPath,
|
||||||
|
OS os, Architecture architecture) {
|
||||||
|
|
||||||
|
public FFmpegSetupConfiguration {
|
||||||
|
Objects.requireNonNull(root);
|
||||||
|
Objects.requireNonNull(bundledFFmpegPath);
|
||||||
|
Objects.requireNonNull(systemFFmpegPath);
|
||||||
|
Objects.requireNonNull(ffmpegInstallerPath);
|
||||||
|
Objects.requireNonNull(ffprobeInstallerPath);
|
||||||
|
Objects.requireNonNull(os);
|
||||||
|
Objects.requireNonNull(architecture);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
package com.github.gtache.autosubtitle.setup.ffmpeg;
|
package com.github.gtache.autosubtitle.setup.ffmpeg;
|
||||||
|
|
||||||
import com.github.gtache.autosubtitle.impl.Architecture;
|
import com.github.gtache.autosubtitle.impl.Architecture;
|
||||||
import com.github.gtache.autosubtitle.impl.OS;
|
|
||||||
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath;
|
|
||||||
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath;
|
|
||||||
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegVersion;
|
|
||||||
import com.github.gtache.autosubtitle.setup.SetupException;
|
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.SetupStatus;
|
||||||
import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
|
import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@@ -13,30 +10,30 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static com.github.gtache.autosubtitle.impl.Architecture.ARMEL;
|
||||||
|
import static com.github.gtache.autosubtitle.impl.Architecture.ARMHF;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manager managing the FFmpeg installation
|
* {@link SetupManager} managing the FFmpeg installation
|
||||||
*/
|
*/
|
||||||
|
//TODO add gpg/signature check
|
||||||
public class FFmpegSetupManager extends AbstractSetupManager {
|
public class FFmpegSetupManager extends AbstractSetupManager {
|
||||||
private static final Logger logger = LogManager.getLogger(FFmpegSetupManager.class);
|
private static final Logger logger = LogManager.getLogger(FFmpegSetupManager.class);
|
||||||
private final Path bundledPath;
|
private final FFmpegSetupConfiguration configuration;
|
||||||
private final Path systemPath;
|
private final Map<String, Decompresser> decompressers;
|
||||||
private final String version;
|
|
||||||
private final OS os;
|
|
||||||
private final Architecture architecture;
|
|
||||||
private final String executableExtension;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
FFmpegSetupManager(@FFmpegBundledPath final Path bundledPath, @FFmpegSystemPath final Path systemPath, @FFmpegVersion final String version, final OS os, final Architecture architecture) {
|
FFmpegSetupManager(final FFmpegSetupConfiguration configuration, final Map<String, Decompresser> decompressers,
|
||||||
this.bundledPath = Objects.requireNonNull(bundledPath);
|
final HttpClient httpClient) {
|
||||||
this.systemPath = Objects.requireNonNull(systemPath);
|
super(httpClient);
|
||||||
this.version = Objects.requireNonNull(version);
|
this.configuration = requireNonNull(configuration);
|
||||||
this.os = Objects.requireNonNull(os);
|
this.decompressers = Map.copyOf(decompressers);
|
||||||
this.architecture = Objects.requireNonNull(architecture);
|
|
||||||
this.executableExtension = os == OS.WINDOWS ? ".exe" : "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -61,12 +58,128 @@ public class FFmpegSetupManager extends AbstractSetupManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void install() throws SetupException {
|
public void install() throws SetupException {
|
||||||
|
switch (configuration.os()) {
|
||||||
|
case WINDOWS -> installWindows();
|
||||||
|
case LINUX -> installLinux();
|
||||||
|
case MAC -> installMac();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installWindows() throws SetupException {
|
||||||
|
final var url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"; //.sha256
|
||||||
|
downloadFFmpeg(url);
|
||||||
|
decompressFFmpeg();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void downloadFFmpeg(final String url) throws SetupException {
|
||||||
|
download(url, configuration.ffmpegInstallerPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void downloadFFProbe(final String url) throws SetupException {
|
||||||
|
download(url, configuration.ffprobeInstallerPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installLinux() throws SetupException {
|
||||||
|
final var url = getLinuxUrl();
|
||||||
|
downloadFFmpeg(url);
|
||||||
|
decompressFFmpegLinux();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decompressFFmpegLinux() throws SetupException {
|
||||||
|
try {
|
||||||
|
final var tmp = Files.createTempFile("ffmpeg", ".tar");
|
||||||
|
decompress(configuration.ffmpegInstallerPath(), tmp);
|
||||||
|
decompress(tmp, configuration.root());
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new SetupException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLinuxUrl() throws SetupException {
|
||||||
|
final var architecture = configuration.architecture();
|
||||||
|
if (architecture.isAMD64()) {
|
||||||
|
return "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"; // .md5
|
||||||
|
} else if (architecture.isARM64()) {
|
||||||
|
return "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz";
|
||||||
|
} else if (architecture == Architecture.I686) {
|
||||||
|
return "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-i686-static.tar.xz";
|
||||||
|
} else if (architecture == ARMHF) {
|
||||||
|
return "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-armhf-static.tar.xz";
|
||||||
|
} else if (architecture == ARMEL) {
|
||||||
|
return "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-armel-static.tar.xz";
|
||||||
|
} else {
|
||||||
|
throwUnsupportedOsArchitectureException();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwUnsupportedOsArchitectureException() throws SetupException {
|
||||||
|
throw new SetupException("Unsupported os - architecture : " + configuration.os() + " - " + configuration.architecture());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installMac() throws SetupException {
|
||||||
|
installFFmpegMac();
|
||||||
|
installFFprobeMac();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installFFmpegMac() throws SetupException {
|
||||||
|
final var url = getMacFFmpegUrl();
|
||||||
|
downloadFFmpeg(url);
|
||||||
|
decompress(configuration.ffmpegInstallerPath(), configuration.root());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decompress(final Path from, final Path to) throws SetupException {
|
||||||
|
try {
|
||||||
|
final var filename = from.getFileName().toString();
|
||||||
|
final var extension = filename.substring(filename.lastIndexOf('.') + 1);
|
||||||
|
decompressers.get(extension).decompress(from, to);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new SetupException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decompressFFmpeg() throws SetupException {
|
||||||
|
decompress(configuration.ffmpegInstallerPath(), configuration.root());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decompressFFProbe() throws SetupException {
|
||||||
|
decompress(configuration.ffprobeInstallerPath(), configuration.root());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installFFprobeMac() throws SetupException {
|
||||||
|
final var url = getMacFFprobeUrl();
|
||||||
|
downloadFFProbe(url);
|
||||||
|
decompressFFProbe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMacFFmpegUrl() throws SetupException {
|
||||||
|
final var architecture = configuration.architecture();
|
||||||
|
if (architecture.isAMD64()) {
|
||||||
|
return "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip"; // /sig
|
||||||
|
} else if (architecture.isARM64()) {
|
||||||
|
return "https://www.osxexperts.net/ffmpeg7arm.zip"; //no automatic sha?
|
||||||
|
} else {
|
||||||
|
throwUnsupportedOsArchitectureException();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMacFFprobeUrl() throws SetupException {
|
||||||
|
final var architecture = configuration.architecture();
|
||||||
|
if (architecture.isAMD64()) {
|
||||||
|
return "https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip";
|
||||||
|
} else if (architecture.isARM64()) {
|
||||||
|
return "https://www.osxexperts.net/ffprobe7arm.zip";
|
||||||
|
} else {
|
||||||
|
throwUnsupportedOsArchitectureException();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void uninstall() throws SetupException {
|
public void uninstall() throws SetupException {
|
||||||
|
deleteFolder(configuration.root());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -75,11 +188,11 @@ public class FFmpegSetupManager extends AbstractSetupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkSystemFFmpeg() throws IOException {
|
private boolean checkSystemFFmpeg() throws IOException {
|
||||||
final var result = run(systemPath.toString(), "-version");
|
final var result = run(configuration.systemFFmpegPath().toString(), "-version");
|
||||||
return result.exitCode() == 0;
|
return result.exitCode() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkBundledFFmpeg() throws IOException {
|
private boolean checkBundledFFmpeg() throws IOException {
|
||||||
return Files.isRegularFile(bundledPath);
|
return Files.isRegularFile(configuration.bundledFFmpegPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.github.gtache.autosubtitle.setup.ffmpeg;
|
||||||
|
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||||
|
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tar implementation of {@link Decompresser}
|
||||||
|
*/
|
||||||
|
public class TarDecompresser implements Decompresser {
|
||||||
|
@Override
|
||||||
|
public void decompress(final Path archive, final Path destination) throws IOException {
|
||||||
|
if (!isPathSupported(archive)) {
|
||||||
|
throw new IllegalArgumentException("Unsupported path : " + archive);
|
||||||
|
}
|
||||||
|
try (final var zis = new TarArchiveInputStream(Files.newInputStream(archive))) {
|
||||||
|
var entry = zis.getNextEntry();
|
||||||
|
while (entry != null) {
|
||||||
|
final var newFile = newFile(destination, entry);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
Files.createDirectories(newFile);
|
||||||
|
} else {
|
||||||
|
// fix for Windows-created archives
|
||||||
|
final var parent = newFile.getParent();
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
|
||||||
|
// write file content
|
||||||
|
try (final var fos = Files.newOutputStream(newFile)) {
|
||||||
|
zis.transferTo(fos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry = zis.getNextEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path newFile(final Path destinationDir, final TarArchiveEntry entry) throws IOException {
|
||||||
|
final var destPath = destinationDir.resolve(entry.getName());
|
||||||
|
|
||||||
|
final var destDirPath = destinationDir.toAbsolutePath().toString();
|
||||||
|
final var destFilePath = destPath.toAbsolutePath().toString();
|
||||||
|
|
||||||
|
if (!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||||
|
throw new IOException("Entry is outside of the target dir: " + entry.getName());
|
||||||
|
}
|
||||||
|
return destPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPathSupported(final Path path) {
|
||||||
|
return path.getFileName().toString().endsWith(".tar");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.github.gtache.autosubtitle.setup.ffmpeg;
|
||||||
|
|
||||||
|
import org.tukaani.xz.XZInputStream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XZ implementation of {@link Decompresser}
|
||||||
|
*/
|
||||||
|
public class XZDecompresser implements Decompresser {
|
||||||
|
@Override
|
||||||
|
public void decompress(final Path archive, final Path destination) throws IOException {
|
||||||
|
if (!isPathSupported(archive)) {
|
||||||
|
throw new IllegalArgumentException("Unsupported path : " + archive);
|
||||||
|
}
|
||||||
|
try (final var xzIn = new XZInputStream(Files.newInputStream(archive));
|
||||||
|
final var out = Files.newOutputStream(destination)) {
|
||||||
|
xzIn.transferTo(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPathSupported(final Path path) {
|
||||||
|
return path.getFileName().toString().endsWith(".xz");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.github.gtache.autosubtitle.setup.ffmpeg;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip implementation of {@link Decompresser}
|
||||||
|
*/
|
||||||
|
public class ZipDecompresser implements Decompresser {
|
||||||
|
@Override
|
||||||
|
public void decompress(final Path archive, final Path destination) throws IOException {
|
||||||
|
if (!isPathSupported(archive)) {
|
||||||
|
throw new IllegalArgumentException("Unsupported path : " + archive);
|
||||||
|
}
|
||||||
|
try (final var zis = new ZipInputStream(Files.newInputStream(archive))) {
|
||||||
|
var zipEntry = zis.getNextEntry();
|
||||||
|
while (zipEntry != null) {
|
||||||
|
final var newFile = newFile(destination, zipEntry);
|
||||||
|
if (zipEntry.isDirectory()) {
|
||||||
|
Files.createDirectories(newFile);
|
||||||
|
} else {
|
||||||
|
// fix for Windows-created archives
|
||||||
|
final var parent = newFile.getParent();
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
|
||||||
|
// write file content
|
||||||
|
try (final var fos = Files.newOutputStream(newFile)) {
|
||||||
|
zis.transferTo(fos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zipEntry = zis.getNextEntry();
|
||||||
|
}
|
||||||
|
zis.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path newFile(final Path destinationDir, final ZipEntry zipEntry) throws IOException {
|
||||||
|
final var destPath = destinationDir.resolve(zipEntry.getName());
|
||||||
|
|
||||||
|
final var destDirPath = destinationDir.toAbsolutePath().toString();
|
||||||
|
final var destFilePath = destPath.toAbsolutePath().toString();
|
||||||
|
|
||||||
|
if (!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||||
|
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
|
||||||
|
}
|
||||||
|
return destPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPathSupported(final Path path) {
|
||||||
|
return path.getFileName().toString().endsWith(".zip");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,10 @@ module com.github.gtache.autosubtitle.ffmpeg {
|
|||||||
requires transitive com.github.gtache.autosubtitle.core;
|
requires transitive com.github.gtache.autosubtitle.core;
|
||||||
requires transitive dagger;
|
requires transitive dagger;
|
||||||
requires transitive javax.inject;
|
requires transitive javax.inject;
|
||||||
|
requires java.net.http;
|
||||||
requires org.apache.logging.log4j;
|
requires org.apache.logging.log4j;
|
||||||
|
requires org.tukaani.xz;
|
||||||
|
requires org.apache.commons.compress;
|
||||||
|
|
||||||
exports com.github.gtache.autosubtitle.ffmpeg;
|
exports com.github.gtache.autosubtitle.ffmpeg;
|
||||||
exports com.github.gtache.autosubtitle.setup.ffmpeg;
|
exports com.github.gtache.autosubtitle.setup.ffmpeg;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.github.gtache.autosubtitle.gui.impl.CombinedResourceBundle;
|
|||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
@@ -13,9 +12,12 @@ import java.util.ResourceBundle;
|
|||||||
* Dagger module for GUI
|
* Dagger module for GUI
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
public class GuiCoreModule {
|
public final class GuiCoreModule {
|
||||||
|
|
||||||
|
private GuiCoreModule() {
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
static ResourceBundle providesBundle() {
|
static ResourceBundle providesBundle() {
|
||||||
return new CombinedResourceBundle(ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.MainBundle"),
|
return new CombinedResourceBundle(ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.MainBundle"),
|
||||||
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.SetupBundle"),
|
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.SetupBundle"),
|
||||||
@@ -25,7 +27,6 @@ public class GuiCoreModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@Play
|
@Play
|
||||||
static byte[] providesPlayImage() {
|
static byte[] providesPlayImage() {
|
||||||
try (final var in = GuiCoreModule.class.getResourceAsStream("/com/github/gtache/autosubtitle/gui/impl/play_64.png")) {
|
try (final var in = GuiCoreModule.class.getResourceAsStream("/com/github/gtache/autosubtitle/gui/impl/play_64.png")) {
|
||||||
@@ -36,7 +37,6 @@ public class GuiCoreModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@Pause
|
@Pause
|
||||||
static byte[] providesPauseImage() {
|
static byte[] providesPauseImage() {
|
||||||
try (final var in = GuiCoreModule.class.getResourceAsStream("/com/github/gtache/autosubtitle/gui/impl/pause_64.png")) {
|
try (final var in = GuiCoreModule.class.getResourceAsStream("/com/github/gtache/autosubtitle/gui/impl/pause_64.png")) {
|
||||||
@@ -47,14 +47,12 @@ public class GuiCoreModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@FontFamily
|
@FontFamily
|
||||||
static String providesFontFamily() {
|
static String providesFontFamily() {
|
||||||
return "Arial";
|
return "Arial";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@FontSize
|
@FontSize
|
||||||
static int providesFontSize() {
|
static int providesFontSize() {
|
||||||
return 12;
|
return 12;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>autosubtitle-fx</artifactId>
|
<artifactId>autosubtitle-gui-fx</artifactId>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<controlsfx.version>11.2.1</controlsfx.version>
|
<controlsfx.version>11.2.1</controlsfx.version>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package com.github.gtache.autosubtitle.gui.fx;
|
package com.github.gtache.autosubtitle.gui.fx;
|
||||||
|
|
||||||
|
import com.github.gtache.autosubtitle.File;
|
||||||
import com.github.gtache.autosubtitle.gui.MediaController;
|
import com.github.gtache.autosubtitle.gui.MediaController;
|
||||||
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
import com.github.gtache.autosubtitle.gui.TimeFormatter;
|
||||||
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
|
|
||||||
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
|
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
|
||||||
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
|
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
|
||||||
|
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
|
||||||
import com.github.gtache.autosubtitle.subtitle.Subtitle;
|
import com.github.gtache.autosubtitle.subtitle.Subtitle;
|
||||||
import com.github.gtache.autosubtitle.subtitle.gui.fx.SubtitleLabel;
|
import com.github.gtache.autosubtitle.subtitle.gui.fx.SubtitleLabel;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
@@ -31,6 +33,11 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
@@ -62,6 +69,8 @@ public class FXMediaController implements MediaController {
|
|||||||
private final Image playImage;
|
private final Image playImage;
|
||||||
private final Image pauseImage;
|
private final Image pauseImage;
|
||||||
|
|
||||||
|
private final List<Long> startTimes;
|
||||||
|
|
||||||
private boolean wasPlaying;
|
private boolean wasPlaying;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -72,6 +81,7 @@ public class FXMediaController implements MediaController {
|
|||||||
this.timeFormatter = requireNonNull(timeFormatter);
|
this.timeFormatter = requireNonNull(timeFormatter);
|
||||||
this.playImage = requireNonNull(playImage);
|
this.playImage = requireNonNull(playImage);
|
||||||
this.pauseImage = requireNonNull(pauseImage);
|
this.pauseImage = requireNonNull(pauseImage);
|
||||||
|
this.startTimes = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -101,22 +111,27 @@ public class FXMediaController implements MediaController {
|
|||||||
if (videoView.getMediaPlayer() != null) {
|
if (videoView.getMediaPlayer() != null) {
|
||||||
videoView.getMediaPlayer().dispose();
|
videoView.getMediaPlayer().dispose();
|
||||||
}
|
}
|
||||||
if (newValue instanceof final FileVideoImpl fileVideo) {
|
if (newValue instanceof final File file) {
|
||||||
final var media = new Media(fileVideo.path().toUri().toString());
|
loadFileVideo(file.path());
|
||||||
final var player = new MediaPlayer(media);
|
} else {
|
||||||
player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> {
|
logger.error("Unsupported video type : {}", newValue);
|
||||||
final var millis = newTime.toMillis();
|
|
||||||
playSlider.setValue(millis);
|
|
||||||
stackPane.getChildren().removeIf(Label.class::isInstance);
|
|
||||||
model.subtitles().forEach(s -> {
|
|
||||||
//TODO optimize using e.g. direction of playback
|
|
||||||
if (s.start() <= millis && s.end() >= millis) {
|
|
||||||
logger.info("Adding label {} at {}", s, millis);
|
|
||||||
final var label = createDraggableLabel(s);
|
|
||||||
stackPane.getChildren().add(label);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
model.subtitles().addListener((ListChangeListener<EditableSubtitle>) c -> {
|
||||||
|
startTimes.clear();
|
||||||
|
model.subtitles().stream().mapToLong(Subtitle::start).forEach(startTimes::add);
|
||||||
});
|
});
|
||||||
|
bindPlayButton();
|
||||||
|
binder.createBindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFileVideo(final Path fileVideoPath) {
|
||||||
|
final var media = new Media(fileVideoPath.toUri().toString());
|
||||||
|
final var player = new MediaPlayer(media);
|
||||||
|
player.statusProperty().addListener((observable12, oldValue1, newValue1) ->
|
||||||
|
logger.info("New status: {}", newValue1));
|
||||||
|
player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> currentTimeChanged(oldTime.toMillis(), newTime.toMillis()));
|
||||||
playSlider.setOnMousePressed(e -> {
|
playSlider.setOnMousePressed(e -> {
|
||||||
wasPlaying = model.isPlaying();
|
wasPlaying = model.isPlaying();
|
||||||
model.setIsPlaying(false);
|
model.setIsPlaying(false);
|
||||||
@@ -140,11 +155,34 @@ public class FXMediaController implements MediaController {
|
|||||||
playSlider.setMax(model.duration());
|
playSlider.setMax(model.duration());
|
||||||
playSlider.setValue(0L);
|
playSlider.setValue(0L);
|
||||||
videoView.setMediaPlayer(player);
|
videoView.setMediaPlayer(player);
|
||||||
} else {
|
}
|
||||||
logger.error("Unsupported video type : {}", newValue);
|
|
||||||
|
private void currentTimeChanged(final double oldMillis, final double millis) {
|
||||||
|
playSlider.setValue(millis);
|
||||||
|
final var subtitleLabels = stackPane.getChildren().stream().filter(SubtitleLabel.class::isInstance).map(SubtitleLabel.class::cast).toList();
|
||||||
|
subtitleLabels.stream().filter(s -> !s.subtitle().isShowing((long) millis)).forEach(sl -> stackPane.getChildren().remove(sl));
|
||||||
|
final var containedSubtitles = subtitleLabels.stream().map(SubtitleLabel::subtitle).filter(s -> s.isShowing((long) millis)).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
model.subtitles().forEach(s -> {
|
||||||
|
if (!containedSubtitles.contains(s)) {
|
||||||
|
logger.info("Adding label {} at {}", s, millis);
|
||||||
|
final var label = createDraggableLabel(s);
|
||||||
|
stackPane.getChildren().add(label);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void currentTimeChangedOptimized(final double oldMillis, final double millis) {
|
||||||
|
final var forward = oldMillis <= millis;
|
||||||
|
|
||||||
|
var index = Collections.binarySearch(startTimes, (long) millis);
|
||||||
|
if (index < 0) {
|
||||||
|
index = forward ? -(index + 1) : -(index + 2);
|
||||||
|
}
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindPlayButton() {
|
||||||
playButton.disableProperty().bind(model.videoProperty().isNull());
|
playButton.disableProperty().bind(model.videoProperty().isNull());
|
||||||
playButton.graphicProperty().bind(Bindings.createObjectBinding(() -> {
|
playButton.graphicProperty().bind(Bindings.createObjectBinding(() -> {
|
||||||
final ImageView view;
|
final ImageView view;
|
||||||
@@ -158,7 +196,6 @@ public class FXMediaController implements MediaController {
|
|||||||
view.setFitHeight(24);
|
view.setFitHeight(24);
|
||||||
return view;
|
return view;
|
||||||
}, model.isPlayingProperty()));
|
}, model.isPlayingProperty()));
|
||||||
binder.createBindings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
|
|||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FX implementation of {@link com.github.gtache.autosubtitle.gui.MediaModel}
|
* FX implementation of {@link com.github.gtache.autosubtitle.gui.MediaModel}
|
||||||
@@ -22,7 +22,7 @@ public class FXMediaModel implements MediaModel {
|
|||||||
private final BooleanProperty isPlaying;
|
private final BooleanProperty isPlaying;
|
||||||
private final ReadOnlyLongWrapper duration;
|
private final ReadOnlyLongWrapper duration;
|
||||||
private final LongProperty position;
|
private final LongProperty position;
|
||||||
private final List<EditableSubtitle> subtitles;
|
private final ObservableList<EditableSubtitle> subtitles;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
FXMediaModel() {
|
FXMediaModel() {
|
||||||
@@ -103,7 +103,7 @@ public class FXMediaModel implements MediaModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<EditableSubtitle> subtitles() {
|
public ObservableList<EditableSubtitle> subtitles() {
|
||||||
return subtitles;
|
return subtitles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(FXWorkController.class);
|
private static final Logger logger = LogManager.getLogger(FXWorkController.class);
|
||||||
|
|
||||||
|
private static final String ALL_SUPPORTED = "All supported";
|
||||||
private static final List<String> VIDEO_EXTENSIONS = Stream.of("webm", "mkv", "flv", "vob", "ogv", "ogg",
|
private static final List<String> VIDEO_EXTENSIONS = Stream.of("webm", "mkv", "flv", "vob", "ogv", "ogg",
|
||||||
"drc", "gif", "gifv", "mng", "avi", "mts", "m2ts", "ts", "mov", "qt", "wmv", "yuv", "rm", "rmvb",
|
"drc", "gif", "gifv", "mng", "avi", "mts", "m2ts", "ts", "mov", "qt", "wmv", "yuv", "rm", "rmvb",
|
||||||
"viv", "asf", "amv", "mp4", "m4p", "m4v", "mpg", "mp2", "mpeg", "mpe", "mpv", "m2v", "m4v", "svi",
|
"viv", "asf", "amv", "mp4", "m4p", "m4v", "mpg", "mp2", "mpeg", "mpe", "mpv", "m2v", "m4v", "svi",
|
||||||
@@ -131,21 +132,42 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void initialize() {
|
private void initialize() {
|
||||||
|
bindComboboxes();
|
||||||
|
bindButtons();
|
||||||
|
bindTable();
|
||||||
|
bindProgress();
|
||||||
|
|
||||||
|
model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (newValue != null) {
|
||||||
|
mediaController.seek(newValue.start());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
binder.createBindings();
|
||||||
|
|
||||||
|
subtitleExtractor.addListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindComboboxes() {
|
||||||
languageCombobox.valueProperty().bindBidirectional(model.videoLanguageProperty());
|
languageCombobox.valueProperty().bindBidirectional(model.videoLanguageProperty());
|
||||||
languageCombobox.setItems(model.availableVideoLanguages());
|
languageCombobox.setItems(model.availableVideoLanguages());
|
||||||
languageCombobox.setConverter(new LanguageStringConverter());
|
languageCombobox.setConverter(new LanguageStringConverter());
|
||||||
translationsCombobox.setConverter(new LanguageStringConverter());
|
translationsCombobox.setConverter(new LanguageStringConverter());
|
||||||
Bindings.bindContent(translationsCombobox.getItems(), model.availableTranslationsLanguage());
|
Bindings.bindContent(translationsCombobox.getItems(), model.availableTranslationsLanguage());
|
||||||
Bindings.bindContent(model.translations(), translationsCombobox.getCheckModel().getCheckedItems());
|
Bindings.bindContent(model.translations(), translationsCombobox.getCheckModel().getCheckedItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindButtons() {
|
||||||
extractButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
extractButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||||
|
addSubtitleButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||||
loadSubtitlesButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
loadSubtitlesButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||||
resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||||
exportSoftButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
exportSoftButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||||
exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||||
addSubtitleButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
saveSubtitlesButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
||||||
loadSubtitlesButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
|
}
|
||||||
|
|
||||||
|
private void bindTable() {
|
||||||
subtitlesTable.setItems(model.subtitles());
|
subtitlesTable.setItems(model.subtitles());
|
||||||
subtitlesTable.setOnKeyPressed(e -> {
|
subtitlesTable.setOnKeyPressed(e -> {
|
||||||
if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
|
if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
|
||||||
@@ -157,6 +179,9 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
} else if (e.getCode() == KeyCode.LEFT) {
|
} else if (e.getCode() == KeyCode.LEFT) {
|
||||||
subtitlesTable.getSelectionModel().selectPrevious();
|
subtitlesTable.getSelectionModel().selectPrevious();
|
||||||
e.consume();
|
e.consume();
|
||||||
|
} else if (e.getCode() == KeyCode.DELETE) {
|
||||||
|
deleteSelectedSubtitles();
|
||||||
|
e.consume();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
|
startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
|
||||||
@@ -185,11 +210,9 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
});
|
});
|
||||||
|
|
||||||
subtitlesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue));
|
subtitlesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue));
|
||||||
model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> {
|
|
||||||
if (newValue != null) {
|
|
||||||
mediaController.seek(newValue.start());
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
private void bindProgress() {
|
||||||
progressLabel.textProperty().bind(Bindings.createStringBinding(() -> resources.getString("work.status." + model.status().name().toLowerCase() + ".label"), model.statusProperty()));
|
progressLabel.textProperty().bind(Bindings.createStringBinding(() -> resources.getString("work.status." + model.status().name().toLowerCase() + ".label"), model.statusProperty()));
|
||||||
progressLabel.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
progressLabel.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||||
progressBar.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
progressBar.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||||
@@ -198,9 +221,10 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
progressBar.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
progressBar.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||||
progressDetailLabel.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
progressDetailLabel.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
|
||||||
progressBar.progressProperty().bindBidirectional(model.progressProperty());
|
progressBar.progressProperty().bindBidirectional(model.progressProperty());
|
||||||
binder.createBindings();
|
}
|
||||||
|
|
||||||
subtitleExtractor.addListener(this);
|
private void deleteSelectedSubtitles() {
|
||||||
|
model.subtitles().removeAll(subtitlesTable.getSelectionModel().getSelectedItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void editFocusedCell() {
|
private void editFocusedCell() {
|
||||||
@@ -213,7 +237,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
@FXML
|
@FXML
|
||||||
private void fileButtonPressed() {
|
private void fileButtonPressed() {
|
||||||
final var filePicker = new FileChooser();
|
final var filePicker = new FileChooser();
|
||||||
final var extensionFilter = new FileChooser.ExtensionFilter("All supported", VIDEO_EXTENSIONS);
|
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, VIDEO_EXTENSIONS);
|
||||||
filePicker.getExtensionFilters().add(extensionFilter);
|
filePicker.getExtensionFilters().add(extensionFilter);
|
||||||
filePicker.setSelectedExtensionFilter(extensionFilter);
|
filePicker.setSelectedExtensionFilter(extensionFilter);
|
||||||
final var file = filePicker.showOpenDialog(window());
|
final var file = filePicker.showOpenDialog(window());
|
||||||
@@ -303,6 +327,9 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
@FXML
|
@FXML
|
||||||
private void exportSoftPressed() {
|
private void exportSoftPressed() {
|
||||||
final var filePicker = new FileChooser();
|
final var filePicker = new FileChooser();
|
||||||
|
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, VIDEO_EXTENSIONS);
|
||||||
|
filePicker.getExtensionFilters().add(extensionFilter);
|
||||||
|
filePicker.setSelectedExtensionFilter(extensionFilter);
|
||||||
final var file = filePicker.showSaveDialog(window());
|
final var file = filePicker.showSaveDialog(window());
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
final var baseCollection = model.subtitleCollection();
|
final var baseCollection = model.subtitleCollection();
|
||||||
@@ -315,7 +342,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
}, Platform::runLater)
|
}, Platform::runLater)
|
||||||
.thenAcceptAsync(collections -> {
|
.thenAcceptAsync(collections -> {
|
||||||
try {
|
try {
|
||||||
videoConverter.addSoftSubtitles(model.video(), collections);
|
videoConverter.addSoftSubtitles(model.video(), collections, file.toPath());
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
throw new CompletionException(e);
|
throw new CompletionException(e);
|
||||||
}
|
}
|
||||||
@@ -332,11 +359,14 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
@FXML
|
@FXML
|
||||||
private void exportHardPressed() {
|
private void exportHardPressed() {
|
||||||
final var filePicker = new FileChooser();
|
final var filePicker = new FileChooser();
|
||||||
|
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, VIDEO_EXTENSIONS);
|
||||||
|
filePicker.getExtensionFilters().add(extensionFilter);
|
||||||
|
filePicker.setSelectedExtensionFilter(extensionFilter);
|
||||||
final var file = filePicker.showSaveDialog(window());
|
final var file = filePicker.showSaveDialog(window());
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
videoConverter.addHardSubtitles(model.video(), model.subtitleCollection());
|
videoConverter.addHardSubtitles(model.video(), model.subtitleCollection(), file.toPath());
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
throw new CompletionException(e);
|
throw new CompletionException(e);
|
||||||
}
|
}
|
||||||
@@ -386,7 +416,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
@FXML
|
@FXML
|
||||||
private void loadSubtitlesPressed() {
|
private void loadSubtitlesPressed() {
|
||||||
final var filePicker = new FileChooser();
|
final var filePicker = new FileChooser();
|
||||||
final var extensionFilter = new FileChooser.ExtensionFilter("All supported", subtitleExtensions);
|
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
|
||||||
filePicker.getExtensionFilters().add(extensionFilter);
|
filePicker.getExtensionFilters().add(extensionFilter);
|
||||||
filePicker.setSelectedExtensionFilter(extensionFilter);
|
filePicker.setSelectedExtensionFilter(extensionFilter);
|
||||||
final var file = filePicker.showOpenDialog(window());
|
final var file = filePicker.showOpenDialog(window());
|
||||||
@@ -396,7 +426,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
|
|||||||
@FXML
|
@FXML
|
||||||
private void saveSubtitlesPressed() {
|
private void saveSubtitlesPressed() {
|
||||||
final var filePicker = new FileChooser();
|
final var filePicker = new FileChooser();
|
||||||
final var extensionFilter = new FileChooser.ExtensionFilter("All supported", subtitleExtensions);
|
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
|
||||||
filePicker.getExtensionFilters().add(extensionFilter);
|
filePicker.getExtensionFilters().add(extensionFilter);
|
||||||
filePicker.setSelectedExtensionFilter(extensionFilter);
|
filePicker.setSelectedExtensionFilter(extensionFilter);
|
||||||
final var file = filePicker.showSaveDialog(window());
|
final var file = filePicker.showSaveDialog(window());
|
||||||
|
|||||||
@@ -75,12 +75,11 @@ public class FXWorkModel implements WorkModel {
|
|||||||
this.extractionModel = new SimpleObjectProperty<>();
|
this.extractionModel = new SimpleObjectProperty<>();
|
||||||
this.progress = new SimpleDoubleProperty(-1);
|
this.progress = new SimpleDoubleProperty(-1);
|
||||||
text.bind(Bindings.createStringBinding(() ->
|
text.bind(Bindings.createStringBinding(() ->
|
||||||
subtitles.stream().map(EditableSubtitle::content).collect(Collectors.joining(" ")),
|
subtitles.stream().map(EditableSubtitle::content).collect(Collectors.joining("")),
|
||||||
subtitles));
|
subtitles));
|
||||||
subtitleCollection.bind(Bindings.createObjectBinding(() -> new SubtitleCollectionImpl(text(), subtitles, videoLanguage())));
|
subtitleCollection.bind(Bindings.createObjectBinding(() -> new SubtitleCollectionImpl(text(), subtitles, videoLanguage()), text, subtitles, videoLanguage));
|
||||||
videoLanguage.addListener((observable, oldValue, newValue) -> {
|
videoLanguage.addListener((observable, oldValue, newValue) -> FXCollections.observableArrayList(Arrays.stream(Language.values())
|
||||||
FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO && l != newValue).sorted(Comparator.naturalOrder()).toList());
|
.filter(l -> l != Language.AUTO && l != newValue).sorted(Comparator.naturalOrder()).toList()));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -54,14 +54,12 @@ public abstract class FXModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@Play
|
@Play
|
||||||
static Image providesPlayImage(@Play final byte[] playImage) {
|
static Image providesPlayImage(@Play final byte[] playImage) {
|
||||||
return new Image(new ByteArrayInputStream(playImage));
|
return new Image(new ByteArrayInputStream(playImage));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@Pause
|
@Pause
|
||||||
static Image providesPauseImage(@Pause final byte[] pauseImage) {
|
static Image providesPauseImage(@Pause final byte[] pauseImage) {
|
||||||
return new Image(new ByteArrayInputStream(pauseImage));
|
return new Image(new ByteArrayInputStream(pauseImage));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* FX module for auto-subtitle
|
* FX module for auto-subtitle
|
||||||
*/
|
*/
|
||||||
module com.github.gtache.autosubtitle.fx {
|
module com.github.gtache.autosubtitle.gui.fx {
|
||||||
requires transitive com.github.gtache.autosubtitle.core;
|
requires transitive com.github.gtache.autosubtitle.core;
|
||||||
requires transitive com.github.gtache.autosubtitle.gui.core;
|
requires transitive com.github.gtache.autosubtitle.gui.core;
|
||||||
requires transitive javafx.controls;
|
requires transitive javafx.controls;
|
||||||
|
|||||||
@@ -1,93 +1,93 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
<?import javafx.geometry.Insets?>
|
<?import javafx.geometry.Insets?>
|
||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.Button?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ProgressBar?>
|
||||||
|
<?import javafx.scene.control.TableColumn?>
|
||||||
|
<?import javafx.scene.control.TableView?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.control.Tooltip?>
|
||||||
|
<?import javafx.scene.layout.ColumnConstraints?>
|
||||||
|
<?import javafx.scene.layout.GridPane?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.RowConstraints?>
|
||||||
<?import org.controlsfx.control.CheckComboBox?>
|
<?import org.controlsfx.control.CheckComboBox?>
|
||||||
<?import org.controlsfx.control.PrefixSelectionComboBox?>
|
<?import org.controlsfx.control.PrefixSelectionComboBox?>
|
||||||
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
|
|
||||||
fx:controller="com.github.gtache.autosubtitle.gui.fx.FXWorkController">
|
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.github.gtache.autosubtitle.gui.fx.FXWorkController">
|
||||||
<columnConstraints>
|
<columnConstraints>
|
||||||
<ColumnConstraints hgrow="SOMETIMES"/>
|
<ColumnConstraints hgrow="SOMETIMES" />
|
||||||
<ColumnConstraints hgrow="ALWAYS"/>
|
<ColumnConstraints hgrow="ALWAYS" />
|
||||||
<ColumnConstraints hgrow="SOMETIMES"/>
|
<ColumnConstraints hgrow="SOMETIMES" />
|
||||||
</columnConstraints>
|
</columnConstraints>
|
||||||
<rowConstraints>
|
<rowConstraints>
|
||||||
<RowConstraints vgrow="SOMETIMES"/>
|
<RowConstraints vgrow="SOMETIMES" />
|
||||||
<RowConstraints vgrow="ALWAYS"/>
|
<RowConstraints vgrow="ALWAYS" />
|
||||||
<RowConstraints vgrow="SOMETIMES"/>
|
<RowConstraints vgrow="SOMETIMES" />
|
||||||
<RowConstraints vgrow="NEVER"/>
|
<RowConstraints vgrow="NEVER" />
|
||||||
</rowConstraints>
|
</rowConstraints>
|
||||||
<children>
|
<children>
|
||||||
<TextField fx:id="fileField" editable="false" GridPane.columnIndex="1"/>
|
<TextField fx:id="fileField" editable="false" GridPane.columnIndex="1" />
|
||||||
<Button mnemonicParsing="false" onAction="#fileButtonPressed" text="%work.button.file.label"
|
<Button mnemonicParsing="false" onAction="#fileButtonPressed" text="%work.button.file.label" GridPane.columnIndex="2" />
|
||||||
GridPane.columnIndex="2"/>
|
|
||||||
<HBox spacing="10.0" GridPane.columnIndex="1" GridPane.rowIndex="2">
|
<HBox spacing="10.0" GridPane.columnIndex="1" GridPane.rowIndex="2">
|
||||||
<children>
|
<children>
|
||||||
<Button fx:id="loadSubtitlesButton" mnemonicParsing="false" onAction="#loadSubtitlesPressed"
|
<Button fx:id="extractButton" mnemonicParsing="false" onAction="#extractPressed" text="%work.button.extract.label" />
|
||||||
text="%work.button.load.label"/>
|
|
||||||
<Button fx:id="extractButton" mnemonicParsing="false" onAction="#extractPressed"
|
|
||||||
text="%work.button.extract.label"/>
|
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
<HBox alignment="CENTER_RIGHT" spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="2">
|
<HBox alignment="CENTER_RIGHT" spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="2">
|
||||||
<children>
|
<children>
|
||||||
<Button fx:id="exportSoftButton" mnemonicParsing="false" onAction="#exportSoftPressed"
|
<Button fx:id="exportSoftButton" mnemonicParsing="false" onAction="#exportSoftPressed" text="%work.button.export.soft.label">
|
||||||
text="%work.button.export.soft.label">
|
|
||||||
<tooltip>
|
<tooltip>
|
||||||
<Tooltip text="%work.button.export.soft.tooltip"/>
|
<Tooltip text="%work.button.export.soft.tooltip" />
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
<Button fx:id="exportHardButton" mnemonicParsing="false" onAction="#exportHardPressed"
|
<Button fx:id="exportHardButton" mnemonicParsing="false" onAction="#exportHardPressed" text="%work.button.export.hard.label">
|
||||||
text="%work.button.export.hard.label">
|
|
||||||
<tooltip>
|
<tooltip>
|
||||||
<Tooltip text="%work.button.export.hard.tooltip"/>
|
<Tooltip text="%work.button.export.hard.tooltip" />
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
<TableView fx:id="subtitlesTable" editable="true" GridPane.rowIndex="1">
|
<TableView fx:id="subtitlesTable" editable="true" GridPane.rowIndex="1">
|
||||||
<columns>
|
<columns>
|
||||||
<TableColumn fx:id="startColumn" prefWidth="50.0" sortable="false"
|
<TableColumn fx:id="startColumn" prefWidth="50.0" sortable="false" text="%work.table.column.from.label" />
|
||||||
text="%work.table.column.from.label"/>
|
<TableColumn fx:id="endColumn" prefWidth="50.0" sortable="false" text="%work.table.column.to.label" />
|
||||||
<TableColumn fx:id="endColumn" prefWidth="50.0" sortable="false" text="%work.table.column.to.label"/>
|
<TableColumn fx:id="textColumn" prefWidth="75.0" sortable="false" text="%work.table.column.text.label" />
|
||||||
<TableColumn fx:id="textColumn" prefWidth="75.0" sortable="false" text="%work.table.column.text.label"/>
|
|
||||||
</columns>
|
</columns>
|
||||||
<columnResizePolicy>
|
<columnResizePolicy>
|
||||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
|
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||||
</columnResizePolicy>
|
</columnResizePolicy>
|
||||||
</TableView>
|
</TableView>
|
||||||
<HBox spacing="10.0" GridPane.rowIndex="2">
|
<HBox spacing="10.0" GridPane.rowIndex="2">
|
||||||
<children>
|
<children>
|
||||||
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed"
|
<Button fx:id="loadSubtitlesButton" mnemonicParsing="false" onAction="#loadSubtitlesPressed" text="%work.button.load.label" />
|
||||||
text="%work.button.reset.label"/>
|
<Button fx:id="saveSubtitlesButton" mnemonicParsing="false" onAction="#saveSubtitlesPressed" text="%work.button.subtitles.save.label" />
|
||||||
<Button fx:id="saveSubtitlesButton" mnemonicParsing="false" onAction="#saveSubtitlesPressed"
|
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed" text="%work.button.reset.label" />
|
||||||
text="%work.button.subtitles.save.label"/>
|
<Button fx:id="addSubtitleButton" mnemonicParsing="false" onAction="#addSubtitlePressed" text="+" />
|
||||||
<Button fx:id="addSubtitleButton" mnemonicParsing="false" onAction="#addSubtitlePressed" text="+"/>
|
|
||||||
</children>
|
</children>
|
||||||
<GridPane.margin>
|
<GridPane.margin>
|
||||||
<Insets/>
|
<Insets />
|
||||||
</GridPane.margin>
|
</GridPane.margin>
|
||||||
</HBox>
|
</HBox>
|
||||||
<fx:include fx:id="media" source="mediaView.fxml" GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
|
<fx:include fx:id="media" source="mediaView.fxml" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="1" />
|
||||||
GridPane.rowIndex="1"/>
|
|
||||||
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
||||||
<children>
|
<children>
|
||||||
<Label text="%work.language.label"/>
|
<Label text="%work.language.label" />
|
||||||
<PrefixSelectionComboBox fx:id="languageCombobox"/>
|
<PrefixSelectionComboBox fx:id="languageCombobox" />
|
||||||
<Label text="%work.translate.label"/>
|
<Label text="%work.translate.label" />
|
||||||
<CheckComboBox fx:id="translationsCombobox"/>
|
<CheckComboBox fx:id="translationsCombobox" />
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
<Label fx:id="progressLabel" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
<Label fx:id="progressLabel" GridPane.columnIndex="1" GridPane.rowIndex="3" />
|
||||||
<HBox spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="3">
|
<HBox spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="3">
|
||||||
<children>
|
<children>
|
||||||
<Label fx:id="progressDetailLabel"/>
|
<Label fx:id="progressDetailLabel" />
|
||||||
<ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS"/>
|
<ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS" />
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
</children>
|
</children>
|
||||||
<padding>
|
<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>
|
</padding>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.gtache.autosubtitle</groupId>
|
<groupId>com.github.gtache.autosubtitle</groupId>
|
||||||
<artifactId>autosubtitle-fx</artifactId>
|
<artifactId>autosubtitle-gui-fx</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.gtache.autosubtitle</groupId>
|
<groupId>com.github.gtache.autosubtitle</groupId>
|
||||||
<artifactId>autosubtitle-fx</artifactId>
|
<artifactId>autosubtitle-gui-fx</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.gtache.autosubtitle</groupId>
|
<groupId>com.github.gtache.autosubtitle</groupId>
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import javax.inject.Singleton;
|
|||||||
@Module
|
@Module
|
||||||
public abstract class MissingComponentsModule {
|
public abstract class MissingComponentsModule {
|
||||||
|
|
||||||
|
private MissingComponentsModule() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
static Translator providesTranslator() {
|
static Translator providesTranslator() {
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegModule;
|
|||||||
import com.github.gtache.autosubtitle.modules.gui.fx.FXModule;
|
import com.github.gtache.autosubtitle.modules.gui.fx.FXModule;
|
||||||
import com.github.gtache.autosubtitle.modules.gui.impl.GuiCoreModule;
|
import com.github.gtache.autosubtitle.modules.gui.impl.GuiCoreModule;
|
||||||
import com.github.gtache.autosubtitle.modules.impl.CoreModule;
|
import com.github.gtache.autosubtitle.modules.impl.CoreModule;
|
||||||
import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFmpegSetupModule;
|
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperSetupModule;
|
|
||||||
import com.github.gtache.autosubtitle.modules.subtitle.impl.SubtitleModule;
|
|
||||||
import com.github.gtache.autosubtitle.modules.whisper.WhisperModule;
|
import com.github.gtache.autosubtitle.modules.whisper.WhisperModule;
|
||||||
import dagger.Component;
|
import dagger.Component;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
@@ -17,8 +14,8 @@ import javax.inject.Singleton;
|
|||||||
* Main component
|
* Main component
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
@Component(modules = {CoreModule.class, GuiCoreModule.class, FXModule.class, FFmpegModule.class, FFmpegSetupModule.class,
|
@Component(modules = {CoreModule.class, GuiCoreModule.class, FXModule.class, FFmpegModule.class,
|
||||||
SubtitleModule.class, WhisperModule.class, WhisperSetupModule.class, MissingComponentsModule.class})
|
WhisperModule.class, MissingComponentsModule.class})
|
||||||
public interface RunComponent {
|
public interface RunComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
module com.github.gtache.autosubtitle.run {
|
module com.github.gtache.autosubtitle.run {
|
||||||
requires com.github.gtache.autosubtitle.ffmpeg;
|
requires com.github.gtache.autosubtitle.ffmpeg;
|
||||||
requires com.github.gtache.autosubtitle.fx;
|
requires com.github.gtache.autosubtitle.gui.fx;
|
||||||
requires com.github.gtache.autosubtitle.whisper;
|
requires com.github.gtache.autosubtitle.whisper;
|
||||||
|
|
||||||
opens com.github.gtache.autosubtitle.run to javafx.graphics;
|
opens com.github.gtache.autosubtitle.run to javafx.graphics;
|
||||||
|
|||||||
18
pom.xml
18
pom.xml
@@ -25,9 +25,11 @@
|
|||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
||||||
|
<commons.compress.version>1.27.0</commons.compress.version>
|
||||||
<dagger.version>2.51.1</dagger.version>
|
<dagger.version>2.51.1</dagger.version>
|
||||||
<log4j.version>2.23.1</log4j.version>
|
<log4j.version>2.23.1</log4j.version>
|
||||||
<picocli.version>4.7.6</picocli.version>
|
<picocli.version>4.7.6</picocli.version>
|
||||||
|
<xz.version>1.10</xz.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@@ -82,6 +84,11 @@
|
|||||||
<artifactId>dagger</artifactId>
|
<artifactId>dagger</artifactId>
|
||||||
<version>${dagger.version}</version>
|
<version>${dagger.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>info.picocli</groupId>
|
||||||
|
<artifactId>picocli</artifactId>
|
||||||
|
<version>${picocli.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
<groupId>org.apache.logging.log4j</groupId>
|
||||||
<artifactId>log4j-api</artifactId>
|
<artifactId>log4j-api</artifactId>
|
||||||
@@ -93,9 +100,14 @@
|
|||||||
<version>${log4j.version}</version>
|
<version>${log4j.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>info.picocli</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>picocli</artifactId>
|
<artifactId>commons-compress</artifactId>
|
||||||
<version>${picocli.version}</version>
|
<version>${commons.compress.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.tukaani</groupId>
|
||||||
|
<artifactId>xz</artifactId>
|
||||||
|
<version>${xz.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.github.gtache.autosubtitle.modules.setup.whisper;
|
package com.github.gtache.autosubtitle.modules.setup.whisper;
|
||||||
|
|
||||||
|
import com.github.gtache.autosubtitle.impl.Architecture;
|
||||||
import com.github.gtache.autosubtitle.impl.OS;
|
import com.github.gtache.autosubtitle.impl.OS;
|
||||||
|
import com.github.gtache.autosubtitle.setup.whisper.CondaSetupConfiguration;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
@@ -13,54 +13,54 @@ import java.nio.file.Paths;
|
|||||||
* Setup module for Conda
|
* Setup module for Conda
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
public abstract class CondaSetupModule {
|
public final class CondaSetupModule {
|
||||||
|
|
||||||
private static final String CONDA = "conda";
|
private static final String CONDA = "conda";
|
||||||
private static final String MINICONDA3 = "miniconda3";
|
private static final String MINICONDA3 = "miniconda3";
|
||||||
|
|
||||||
@Provides
|
private CondaSetupModule() {
|
||||||
@Singleton
|
|
||||||
static HttpClient providesHttpClient() {
|
}
|
||||||
return HttpClient.newHttpClient();
|
|
||||||
|
@Provides
|
||||||
|
static CondaSetupConfiguration providesCondaSetupConfiguration(@CondaSystemPath final Path condaSystemPath, @CondaBundledPath final Path condaBundledPath,
|
||||||
|
@CondaMinimumMajorVersion final int condaMinimumMajorVersion, @CondaMinimumMinorVersion final int condaMinimumMinorVersion,
|
||||||
|
@CondaInstallerPath final Path condaInstallerPath, @CondaRootPath final Path condaRootPath,
|
||||||
|
final OS os, final Architecture architecture) {
|
||||||
|
return new CondaSetupConfiguration(condaRootPath, condaSystemPath, condaBundledPath, condaMinimumMajorVersion, condaMinimumMinorVersion, condaInstallerPath, os, architecture);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@CondaSystemPath
|
@CondaSystemPath
|
||||||
static Path providesCondaSystemPath(final OS os) {
|
static Path providesCondaSystemPath(final OS os) {
|
||||||
return Paths.get(os == OS.WINDOWS ? CONDA + ".bat" : CONDA);
|
return Paths.get(os == OS.WINDOWS ? CONDA + ".bat" : CONDA);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@CondaBundledPath
|
@CondaBundledPath
|
||||||
static Path providesCondaBundledPath(@CondaRootPath final Path root, final OS os) {
|
static Path providesCondaBundledPath(@CondaRootPath final Path root, final OS os) {
|
||||||
return root.resolve("condabin").resolve(Paths.get(os == OS.WINDOWS ? CONDA + ".bat" : CONDA));
|
return root.resolve("condabin").resolve(Paths.get(os == OS.WINDOWS ? CONDA + ".bat" : CONDA));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@CondaMinimumMajorVersion
|
@CondaMinimumMajorVersion
|
||||||
static int providesCondaMinimumMajorVersion() {
|
static int providesCondaMinimumMajorVersion() {
|
||||||
return 24;
|
return 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@CondaMinimumMinorVersion
|
@CondaMinimumMinorVersion
|
||||||
static int providesCondaMinimumMinorVersion() {
|
static int providesCondaMinimumMinorVersion() {
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@CondaRootPath
|
@CondaRootPath
|
||||||
static Path providesCondaRootPath(@WhisperBundledRoot final Path root, final OS os) {
|
static Path providesCondaRootPath(@WhisperBundledRoot final Path root, final OS os) {
|
||||||
return root.resolve(MINICONDA3);
|
return root.resolve(MINICONDA3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@CondaInstallerPath
|
@CondaInstallerPath
|
||||||
static Path providesCondaInstallerPath(@WhisperBundledRoot final Path root, final OS os) {
|
static Path providesCondaInstallerPath(@WhisperBundledRoot final Path root, final OS os) {
|
||||||
return root.resolve("cache").resolve("conda-install" + (os == OS.WINDOWS ? ".exe" : ".sh"));
|
return root.resolve("cache").resolve("conda-install" + (os == OS.WINDOWS ? ".exe" : ".sh"));
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import dagger.Binds;
|
|||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
@@ -21,28 +20,24 @@ public abstract class WhisperSetupModule {
|
|||||||
abstract SetupManager bindsSubtitleExtractorSetupManager(final WhisperSetupManager manager);
|
abstract SetupManager bindsSubtitleExtractorSetupManager(final WhisperSetupManager manager);
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@PythonVersion
|
@PythonVersion
|
||||||
static String providesPythonVersion() {
|
static String providesPythonVersion() {
|
||||||
return "3.9.19";
|
return "3.9.19";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@WhisperVersion
|
@WhisperVersion
|
||||||
static String providesWhisperVersion() {
|
static String providesWhisperVersion() {
|
||||||
return "20231117";
|
return "20231117";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@WhisperBundledRoot
|
@WhisperBundledRoot
|
||||||
static Path providesWhisperBundledRoot() {
|
static Path providesWhisperBundledRoot() {
|
||||||
return Paths.get("tools", "whisper");
|
return Paths.get("tools", "whisper");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
|
||||||
@WhisperVenvPath
|
@WhisperVenvPath
|
||||||
static Path providesWhisperVenvPath(@WhisperBundledRoot final Path root) {
|
static Path providesWhisperVenvPath(@WhisperBundledRoot final Path root) {
|
||||||
return root.resolve("whisper-env");
|
return root.resolve("whisper-env");
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import dagger.Module;
|
|||||||
@Module
|
@Module
|
||||||
public abstract class WhisperExtractorModule {
|
public abstract class WhisperExtractorModule {
|
||||||
|
|
||||||
|
private WhisperExtractorModule() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract SubtitleExtractor bindsSubtitleExtractor(final WhisperSubtitleExtractor extractor);
|
abstract SubtitleExtractor bindsSubtitleExtractor(final WhisperSubtitleExtractor extractor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import javax.inject.Singleton;
|
|||||||
@Module
|
@Module
|
||||||
public abstract class WhisperJsonModule {
|
public abstract class WhisperJsonModule {
|
||||||
|
|
||||||
|
private WhisperJsonModule() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@StringKey("json")
|
@StringKey("json")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.github.gtache.autosubtitle.modules.whisper;
|
package com.github.gtache.autosubtitle.modules.whisper;
|
||||||
|
|
||||||
|
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperSetupModule;
|
||||||
import com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper.WhisperExtractorModule;
|
import com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper.WhisperExtractorModule;
|
||||||
import com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisper.WhisperJsonModule;
|
import com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisper.WhisperJsonModule;
|
||||||
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
|
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
|
||||||
@@ -10,9 +11,13 @@ import dagger.Module;
|
|||||||
/**
|
/**
|
||||||
* Dagger module for Whisper
|
* Dagger module for Whisper
|
||||||
*/
|
*/
|
||||||
@Module(includes = {WhisperJsonModule.class, WhisperExtractorModule.class})
|
@Module(includes = {WhisperSetupModule.class, WhisperJsonModule.class, WhisperExtractorModule.class})
|
||||||
public abstract class WhisperModule {
|
public abstract class WhisperModule {
|
||||||
|
|
||||||
|
private WhisperModule() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract ExtractionModelProvider bindsExtractionModelProvider(final WhisperExtractionModelProvider provider);
|
abstract ExtractionModelProvider bindsExtractionModelProvider(final WhisperExtractionModelProvider provider);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.github.gtache.autosubtitle.setup.whisper;
|
||||||
|
|
||||||
|
import com.github.gtache.autosubtitle.impl.Architecture;
|
||||||
|
import com.github.gtache.autosubtitle.impl.OS;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for conda setup
|
||||||
|
*/
|
||||||
|
public record CondaSetupConfiguration(Path condaRootPath, Path condaSystemPath, Path condaBundledPath,
|
||||||
|
int condaMinimumMajorVersion, int condaMinimumMinorVersion,
|
||||||
|
Path condaInstallerPath, OS os, Architecture architecture) {
|
||||||
|
|
||||||
|
public CondaSetupConfiguration {
|
||||||
|
Objects.requireNonNull(condaRootPath);
|
||||||
|
Objects.requireNonNull(condaSystemPath);
|
||||||
|
Objects.requireNonNull(condaBundledPath);
|
||||||
|
if (condaMinimumMajorVersion <= 0) {
|
||||||
|
throw new IllegalArgumentException("Conda minimum major version must be > 0 : " + condaMinimumMajorVersion);
|
||||||
|
}
|
||||||
|
if (condaMinimumMinorVersion < 0) {
|
||||||
|
throw new IllegalArgumentException("Conda minimum minor version must be >= 0 : " + condaMinimumMinorVersion);
|
||||||
|
}
|
||||||
|
Objects.requireNonNull(condaInstallerPath);
|
||||||
|
Objects.requireNonNull(os);
|
||||||
|
Objects.requireNonNull(architecture);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
package com.github.gtache.autosubtitle.setup.whisper;
|
package com.github.gtache.autosubtitle.setup.whisper;
|
||||||
|
|
||||||
import com.github.gtache.autosubtitle.impl.Architecture;
|
|
||||||
import com.github.gtache.autosubtitle.impl.OS;
|
import com.github.gtache.autosubtitle.impl.OS;
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.CondaBundledPath;
|
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.CondaInstallerPath;
|
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.CondaMinimumMajorVersion;
|
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.CondaMinimumMinorVersion;
|
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.CondaRootPath;
|
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.CondaSystemPath;
|
|
||||||
import com.github.gtache.autosubtitle.setup.SetupException;
|
import com.github.gtache.autosubtitle.setup.SetupException;
|
||||||
import com.github.gtache.autosubtitle.setup.SetupStatus;
|
import com.github.gtache.autosubtitle.setup.SetupStatus;
|
||||||
import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
|
import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
|
||||||
@@ -17,10 +10,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -36,30 +26,12 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(CondaSetupManager.class);
|
private static final Logger logger = LogManager.getLogger(CondaSetupManager.class);
|
||||||
|
|
||||||
private final Path condaSystemPath;
|
private final CondaSetupConfiguration configuration;
|
||||||
private final Path condaBundledPath;
|
|
||||||
private final int condaMinimumMajorVersion;
|
|
||||||
private final int condaMinimumMinorVersion;
|
|
||||||
private final Path condaInstallerPath;
|
|
||||||
private final Path condaRootPath;
|
|
||||||
private final OS os;
|
|
||||||
private final Architecture architecture;
|
|
||||||
private final HttpClient httpClient;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
CondaSetupManager(@CondaSystemPath final Path condaSystemPath, @CondaBundledPath final Path condaBundledPath,
|
CondaSetupManager(final CondaSetupConfiguration configuration, final HttpClient httpClient) {
|
||||||
@CondaMinimumMajorVersion final int condaMinimumMajorVersion, @CondaMinimumMinorVersion final int condaMinimumMinorVersion,
|
super(httpClient);
|
||||||
@CondaInstallerPath final Path condaInstallerPath, @CondaRootPath final Path condaRootPath,
|
this.configuration = requireNonNull(configuration);
|
||||||
final OS os, final Architecture architecture, final HttpClient httpClient) {
|
|
||||||
this.condaSystemPath = requireNonNull(condaSystemPath);
|
|
||||||
this.condaBundledPath = requireNonNull(condaBundledPath);
|
|
||||||
this.condaMinimumMajorVersion = condaMinimumMajorVersion;
|
|
||||||
this.condaMinimumMinorVersion = condaMinimumMinorVersion;
|
|
||||||
this.condaInstallerPath = requireNonNull(condaInstallerPath);
|
|
||||||
this.condaRootPath = requireNonNull(condaRootPath);
|
|
||||||
this.os = requireNonNull(os);
|
|
||||||
this.architecture = requireNonNull(architecture);
|
|
||||||
this.httpClient = requireNonNull(httpClient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -86,14 +58,15 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void installConda() throws SetupException {
|
private void installConda() throws SetupException {
|
||||||
if (Files.exists(condaInstallerPath)) {
|
final var installerPath = configuration.condaInstallerPath();
|
||||||
logger.info("Conda exists at {}", condaInstallerPath);
|
if (Files.exists(installerPath)) {
|
||||||
|
logger.info("Conda exists at {}", installerPath);
|
||||||
} else {
|
} else {
|
||||||
logger.info("Conda installer not found, downloading");
|
logger.info("Conda installer not found, downloading");
|
||||||
downloadConda();
|
downloadConda();
|
||||||
logger.info("Conda downloaded");
|
logger.info("Conda downloaded");
|
||||||
}
|
}
|
||||||
switch (os) {
|
switch (configuration.os()) {
|
||||||
case WINDOWS -> installWindows();
|
case WINDOWS -> installWindows();
|
||||||
case MAC, LINUX -> installLinux();
|
case MAC, LINUX -> installLinux();
|
||||||
}
|
}
|
||||||
@@ -101,10 +74,12 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
|
|
||||||
private void installLinux() throws SetupException {
|
private void installLinux() throws SetupException {
|
||||||
try {
|
try {
|
||||||
logger.info("Installing conda using {}", condaInstallerPath);
|
final var installerPath = configuration.condaInstallerPath();
|
||||||
final var result = run("bash", condaInstallerPath.toString(), "-b", "-p", condaRootPath.toString());
|
final var rootPath = configuration.condaRootPath();
|
||||||
|
logger.info("Installing conda using {}", installerPath);
|
||||||
|
final var result = run("bash", installerPath.toString(), "-b", "-p", rootPath.toString());
|
||||||
if (result.exitCode() == 0) {
|
if (result.exitCode() == 0) {
|
||||||
logger.info("Installed conda to {}", condaRootPath);
|
logger.info("Installed conda to {}", rootPath);
|
||||||
} else {
|
} else {
|
||||||
throw new SetupException("Error installing conda: " + result);
|
throw new SetupException("Error installing conda: " + result);
|
||||||
}
|
}
|
||||||
@@ -115,10 +90,12 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
|
|
||||||
private void installWindows() throws SetupException {
|
private void installWindows() throws SetupException {
|
||||||
try {
|
try {
|
||||||
logger.info("Installing conda using {}", condaInstallerPath);
|
final var installerPath = configuration.condaInstallerPath();
|
||||||
final var result = run(condaInstallerPath.toString(), "/InstallationType=JustMe", "/RegisterPython=0", "/S", "/D=" + condaRootPath.toString());
|
final var rootPath = configuration.condaRootPath();
|
||||||
|
logger.info("Installing conda using {}", installerPath);
|
||||||
|
final var result = run(installerPath.toString(), "/InstallationType=JustMe", "/RegisterPython=0", "/S", "/D=" + rootPath.toString());
|
||||||
if (result.exitCode() == 0) {
|
if (result.exitCode() == 0) {
|
||||||
logger.info("Installed conda to {}", condaRootPath);
|
logger.info("Installed conda to {}", rootPath);
|
||||||
} else {
|
} else {
|
||||||
throw new SetupException("Error installing conda: " + result);
|
throw new SetupException("Error installing conda: " + result);
|
||||||
}
|
}
|
||||||
@@ -128,15 +105,16 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void downloadConda() throws SetupException {
|
private void downloadConda() throws SetupException {
|
||||||
switch (os) {
|
switch (configuration.os()) {
|
||||||
case WINDOWS -> downloadCondaWindows();
|
case WINDOWS -> downloadCondaWindows();
|
||||||
case MAC -> downloadCondaMac();
|
case MAC -> downloadCondaMac();
|
||||||
case LINUX -> downloadCondaLinux();
|
case LINUX -> downloadCondaLinux();
|
||||||
}
|
}
|
||||||
logger.info("Downloaded conda to {}", condaInstallerPath);
|
logger.info("Downloaded conda to {}", configuration.condaInstallerPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void downloadCondaLinux() throws SetupException {
|
private void downloadCondaLinux() throws SetupException {
|
||||||
|
final var architecture = configuration.architecture();
|
||||||
if (architecture.isAMD64()) {
|
if (architecture.isAMD64()) {
|
||||||
downloadConda("https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh");
|
downloadConda("https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh");
|
||||||
} else if (architecture.isARM64()) {
|
} else if (architecture.isARM64()) {
|
||||||
@@ -147,6 +125,7 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void downloadCondaMac() throws SetupException {
|
private void downloadCondaMac() throws SetupException {
|
||||||
|
final var architecture = configuration.architecture();
|
||||||
if (architecture.isAMD64()) {
|
if (architecture.isAMD64()) {
|
||||||
downloadConda("https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh");
|
downloadConda("https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh");
|
||||||
} else if (architecture.isARM64()) {
|
} else if (architecture.isARM64()) {
|
||||||
@@ -162,25 +141,12 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void downloadConda(final String url) throws SetupException {
|
private void downloadConda(final String url) throws SetupException {
|
||||||
final var request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
|
download(url, configuration.condaInstallerPath());
|
||||||
try {
|
|
||||||
final var result = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(condaInstallerPath));
|
|
||||||
if (result.statusCode() == 200) {
|
|
||||||
logger.info("Conda download successful");
|
|
||||||
} else {
|
|
||||||
throw new SetupException("Error downloading conda: " + result.body());
|
|
||||||
}
|
|
||||||
} catch (final IOException e) {
|
|
||||||
throw new SetupException(e);
|
|
||||||
} catch (final InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new SetupException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void uninstall() throws SetupException {
|
public void uninstall() throws SetupException {
|
||||||
deleteFolder(condaRootPath);
|
deleteFolder(configuration.condaRootPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -208,7 +174,7 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
throw new SetupException("Error creating venv " + path + ": " + result.output());
|
throw new SetupException("Error creating venv " + path + ": " + result.output());
|
||||||
}
|
}
|
||||||
// On Windows, we need to copy the DLLs otherwise pip may not work
|
// On Windows, we need to copy the DLLs otherwise pip may not work
|
||||||
if (os == OS.WINDOWS) {
|
if (configuration.os() == OS.WINDOWS) {
|
||||||
final var sourceFolder = path.resolve("Library").resolve("bin");
|
final var sourceFolder = path.resolve("Library").resolve("bin");
|
||||||
try (final var files = Files.find(sourceFolder, 1, (p, a) -> p.getFileName().toString().contains("libcrypto") || p.getFileName().toString().contains("libssl"))) {
|
try (final var files = Files.find(sourceFolder, 1, (p, a) -> p.getFileName().toString().contains("libcrypto") || p.getFileName().toString().contains("libssl"))) {
|
||||||
final var fileList = files.toList();
|
final var fileList = files.toList();
|
||||||
@@ -225,16 +191,16 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean venvExists(final Path path) {
|
public boolean venvExists(final Path path) {
|
||||||
return Files.exists(path.resolve("bin").resolve(os == OS.WINDOWS ? "python.exe" : "python"));
|
return Files.exists(path.resolve("bin").resolve(configuration.os() == OS.WINDOWS ? "python.exe" : "python"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path getCondaPath() throws SetupException {
|
private Path getCondaPath() throws SetupException {
|
||||||
return isSystemCondaInstalled() ? condaSystemPath : condaBundledPath;
|
return isSystemCondaInstalled() ? configuration.condaSystemPath() : configuration.condaBundledPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSystemCondaInstalled() throws SetupException {
|
private boolean isSystemCondaInstalled() throws SetupException {
|
||||||
try {
|
try {
|
||||||
final var result = run(condaSystemPath.toString(), "--version");
|
final var result = run(configuration.condaSystemPath().toString(), "--version");
|
||||||
if (result.exitCode() == 0) {
|
if (result.exitCode() == 0) {
|
||||||
final var output = result.output().getFirst();
|
final var output = result.output().getFirst();
|
||||||
final var versionString = output.substring(output.indexOf(' ') + 1);
|
final var versionString = output.substring(output.indexOf(' ') + 1);
|
||||||
@@ -242,7 +208,7 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
if (version.length == 3) {
|
if (version.length == 3) {
|
||||||
final var major = Integer.parseInt(version[0]);
|
final var major = Integer.parseInt(version[0]);
|
||||||
final var minor = Integer.parseInt(version[1]);
|
final var minor = Integer.parseInt(version[1]);
|
||||||
return major >= condaMinimumMajorVersion && minor >= condaMinimumMinorVersion;
|
return major >= configuration.condaMinimumMajorVersion() && minor >= configuration.condaMinimumMinorVersion();
|
||||||
} else {
|
} else {
|
||||||
throw new SetupException("Unexpected python version: " + versionString);
|
throw new SetupException("Unexpected python version: " + versionString);
|
||||||
}
|
}
|
||||||
@@ -255,6 +221,6 @@ public class CondaSetupManager extends AbstractSetupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isBundledCondaInstalled() {
|
private boolean isBundledCondaInstalled() {
|
||||||
return Files.isRegularFile(condaBundledPath);
|
return Files.isRegularFile(configuration.condaBundledPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.github.gtache.autosubtitle.setup.whisper;
|
||||||
|
|
||||||
|
import com.github.gtache.autosubtitle.impl.OS;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for whisper setup
|
||||||
|
*/
|
||||||
|
public record WhisperSetupConfiguration(Path root, Path venvPath, String pythonVersion,
|
||||||
|
String whisperVersion, OS os) {
|
||||||
|
|
||||||
|
public WhisperSetupConfiguration {
|
||||||
|
Objects.requireNonNull(root);
|
||||||
|
Objects.requireNonNull(venvPath);
|
||||||
|
Objects.requireNonNull(pythonVersion);
|
||||||
|
Objects.requireNonNull(whisperVersion);
|
||||||
|
Objects.requireNonNull(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
package com.github.gtache.autosubtitle.setup.whisper;
|
package com.github.gtache.autosubtitle.setup.whisper;
|
||||||
|
|
||||||
import com.github.gtache.autosubtitle.impl.OS;
|
import com.github.gtache.autosubtitle.impl.OS;
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.PythonVersion;
|
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperBundledRoot;
|
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
|
|
||||||
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVersion;
|
|
||||||
import com.github.gtache.autosubtitle.setup.SetupAction;
|
import com.github.gtache.autosubtitle.setup.SetupAction;
|
||||||
import com.github.gtache.autosubtitle.setup.SetupException;
|
import com.github.gtache.autosubtitle.setup.SetupException;
|
||||||
import com.github.gtache.autosubtitle.setup.SetupStatus;
|
import com.github.gtache.autosubtitle.setup.SetupStatus;
|
||||||
@@ -20,28 +16,23 @@ import java.nio.file.Path;
|
|||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
//TODO add gpg/signature check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup manager for Whisper
|
||||||
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
public class WhisperSetupManager extends AbstractSetupManager {
|
public class WhisperSetupManager extends AbstractSetupManager {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(WhisperSetupManager.class);
|
private static final Logger logger = LogManager.getLogger(WhisperSetupManager.class);
|
||||||
private static final String CONDA_ENV = "conda-env";
|
private static final String CONDA_ENV = "conda-env";
|
||||||
private final CondaSetupManager condaSetupManager;
|
private final CondaSetupManager condaSetupManager;
|
||||||
private final String pythonVersion;
|
private final WhisperSetupConfiguration configuration;
|
||||||
private final Path whisperRoot;
|
|
||||||
private final Path venvPath;
|
|
||||||
private final OS os;
|
|
||||||
private final String whisperVersion;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
WhisperSetupManager(final CondaSetupManager condaSetupManager, @PythonVersion final String pythonVersion,
|
WhisperSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration) {
|
||||||
@WhisperBundledRoot final Path whisperRoot, @WhisperVenvPath final Path venvPath, final OS os,
|
|
||||||
@WhisperVersion final String whisperVersion) {
|
|
||||||
this.condaSetupManager = requireNonNull(condaSetupManager);
|
this.condaSetupManager = requireNonNull(condaSetupManager);
|
||||||
this.pythonVersion = requireNonNull(pythonVersion);
|
this.configuration = requireNonNull(configuration);
|
||||||
this.whisperRoot = requireNonNull(whisperRoot);
|
|
||||||
this.venvPath = requireNonNull(venvPath);
|
|
||||||
this.os = requireNonNull(os);
|
|
||||||
this.whisperVersion = requireNonNull(whisperVersion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -83,12 +74,12 @@ public class WhisperSetupManager extends AbstractSetupManager {
|
|||||||
|
|
||||||
private void checkInstallVenv() throws SetupException {
|
private void checkInstallVenv() throws SetupException {
|
||||||
sendStartEvent(SetupAction.CHECK, CONDA_ENV, 0.33);
|
sendStartEvent(SetupAction.CHECK, CONDA_ENV, 0.33);
|
||||||
if (condaSetupManager.venvExists(venvPath)) {
|
if (condaSetupManager.venvExists(configuration.venvPath())) {
|
||||||
sendEndEvent(SetupAction.CHECK, CONDA_ENV, 0.66);
|
sendEndEvent(SetupAction.CHECK, CONDA_ENV, 0.66);
|
||||||
} else {
|
} else {
|
||||||
sendEndEvent(SetupAction.CHECK, CONDA_ENV, -1);
|
sendEndEvent(SetupAction.CHECK, CONDA_ENV, -1);
|
||||||
sendStartEvent(SetupAction.INSTALL, CONDA_ENV, -1);
|
sendStartEvent(SetupAction.INSTALL, CONDA_ENV, -1);
|
||||||
condaSetupManager.createVenv(venvPath, pythonVersion);
|
condaSetupManager.createVenv(configuration.venvPath(), configuration.pythonVersion());
|
||||||
sendEndEvent(SetupAction.INSTALL, CONDA_ENV, 0.66);
|
sendEndEvent(SetupAction.INSTALL, CONDA_ENV, 0.66);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +98,7 @@ public class WhisperSetupManager extends AbstractSetupManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void uninstall() throws SetupException {
|
public void uninstall() throws SetupException {
|
||||||
deleteFolder(whisperRoot);
|
deleteFolder(configuration.root());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -124,19 +115,21 @@ public class WhisperSetupManager extends AbstractSetupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Path getPythonPath() {
|
private Path getPythonPath() {
|
||||||
return venvPath.resolve(os == OS.WINDOWS ? "python.exe" : "python");
|
return configuration.venvPath().resolve(configuration.os() == OS.WINDOWS ? "python.exe" : "python");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void installWhisper() throws SetupException {
|
private void installWhisper() throws SetupException {
|
||||||
final var path = getPythonPath();
|
final var path = getPythonPath();
|
||||||
try {
|
try {
|
||||||
logger.info("Installing whisper");
|
logger.info("Installing whisper");
|
||||||
final var result = run(path.toString(), "-m", "pip", "install", "-U", "openai-whisper==" + whisperVersion, "numpy<2");
|
final var result = run(path.toString(), "-m", "pip", "install", "-U", "openai-whisper==" + configuration.whisperVersion(), "numpy<2");
|
||||||
if (result.exitCode() == 0) {
|
if (result.exitCode() == 0) {
|
||||||
logger.info("Whisper installed");
|
logger.info("Whisper installed");
|
||||||
} else {
|
} else {
|
||||||
throw new SetupException("Error installing whisper: " + result.output());
|
throw new SetupException("Error installing whisper: " + result.output());
|
||||||
}
|
}
|
||||||
|
//TODO cuda?
|
||||||
|
final var cudaResult = run(path.toString(), "-m", "pip", "install", "-U", "torch", "--index-url", "https://download.pytorch.org/whl/cu124");
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
throw new SetupException(e);
|
throw new SetupException(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ import static java.util.Objects.requireNonNull;
|
|||||||
public class WhisperSubtitleExtractor extends AbstractProcessRunner implements SubtitleExtractor {
|
public class WhisperSubtitleExtractor extends AbstractProcessRunner implements SubtitleExtractor {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(WhisperSubtitleExtractor.class);
|
private static final Logger logger = LogManager.getLogger(WhisperSubtitleExtractor.class);
|
||||||
|
|
||||||
|
private static final String AUTOSUBTITLE = "autosubtitle";
|
||||||
private static final Pattern LINE_PROGRESS_PATTERN = Pattern.compile("^\\[\\d{2}:\\d{2}\\.\\d{3} --> (?<minutes>\\d{2}):(?<seconds>\\d{2})\\.(?<millis>\\d{3})]");
|
private static final Pattern LINE_PROGRESS_PATTERN = Pattern.compile("^\\[\\d{2}:\\d{2}\\.\\d{3} --> (?<minutes>\\d{2}):(?<seconds>\\d{2})\\.(?<millis>\\d{3})]");
|
||||||
|
private static final Pattern TQDM_PROGRESS_PATTERN = Pattern.compile("^(?<progress>\\d{1,3})%|.+");
|
||||||
private final Path venvPath;
|
private final Path venvPath;
|
||||||
private final SubtitleConverter converter;
|
private final SubtitleConverter converter;
|
||||||
private final OS os;
|
private final OS os;
|
||||||
@@ -80,7 +83,7 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
|
|||||||
return extract(f.path(), language, model, video.info().duration());
|
return extract(f.path(), language, model, video.info().duration());
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
final var path = Files.createTempFile("autosubtitle", video.info().videoFormat());
|
final var path = Files.createTempFile(AUTOSUBTITLE, video.info().videoFormat());
|
||||||
try (final var in = video.getInputStream()) {
|
try (final var in = video.getInputStream()) {
|
||||||
Files.copy(in, path);
|
Files.copy(in, path);
|
||||||
final var ret = extract(path, language, model, video.info().duration());
|
final var ret = extract(path, language, model, video.info().duration());
|
||||||
@@ -99,7 +102,7 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
|
|||||||
return extract(f.path(), language, model, audio.info().duration());
|
return extract(f.path(), language, model, audio.info().duration());
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
final var path = Files.createTempFile("autosubtitle", audio.info().audioFormat());
|
final var path = Files.createTempFile(AUTOSUBTITLE, audio.info().audioFormat());
|
||||||
try (final var in = audio.getInputStream()) {
|
try (final var in = audio.getInputStream()) {
|
||||||
Files.copy(in, path);
|
Files.copy(in, path);
|
||||||
final var ret = extract(path, language, model, audio.info().duration());
|
final var ret = extract(path, language, model, audio.info().duration());
|
||||||
@@ -114,13 +117,13 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
|
|||||||
|
|
||||||
private SubtitleCollection extract(final Path path, final Language language, final ExtractionModel model, final long duration) throws ExtractException {
|
private SubtitleCollection extract(final Path path, final Language language, final ExtractionModel model, final long duration) throws ExtractException {
|
||||||
try {
|
try {
|
||||||
final var outputDir = Files.createTempDirectory("autosubtitle");
|
final var outputDir = Files.createTempDirectory(AUTOSUBTITLE);
|
||||||
final var args = createArgs(path, language, model, outputDir);
|
final var args = createArgs(path, language, model, outputDir);
|
||||||
final var processListener = startListen(args);
|
final var processListener = startListen(args);
|
||||||
var line = processListener.readLine();
|
|
||||||
var oldProgress = -1.0;
|
var oldProgress = -1.0;
|
||||||
|
var line = processListener.readLine();
|
||||||
while (line != null) {
|
while (line != null) {
|
||||||
logger.info("Whisper output : {}", line);
|
logger.info("[whisper]: {}", line);
|
||||||
final var newProgress = computeProgress(line, duration, oldProgress);
|
final var newProgress = computeProgress(line, duration, oldProgress);
|
||||||
notifyListeners(new ExtractEventImpl(line, newProgress));
|
notifyListeners(new ExtractEventImpl(line, newProgress));
|
||||||
oldProgress = newProgress;
|
oldProgress = newProgress;
|
||||||
@@ -131,11 +134,7 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
|
|||||||
final var filename = path.getFileName().toString();
|
final var filename = path.getFileName().toString();
|
||||||
final var subtitleFilename = filename.substring(0, filename.lastIndexOf('.')) + ".json";
|
final var subtitleFilename = filename.substring(0, filename.lastIndexOf('.')) + ".json";
|
||||||
final var subtitleFile = outputDir.resolve(subtitleFilename);
|
final var subtitleFile = outputDir.resolve(subtitleFilename);
|
||||||
try {
|
return parseResult(subtitleFile);
|
||||||
return converter.parse(subtitleFile);
|
|
||||||
} catch (final ParseException e) {
|
|
||||||
throw new ExtractException(e);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new ExtractException("Error extracting subtitles: " + result.output());
|
throw new ExtractException("Error extracting subtitles: " + result.output());
|
||||||
}
|
}
|
||||||
@@ -144,6 +143,14 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SubtitleCollection parseResult(final Path subtitleFile) throws ExtractException {
|
||||||
|
try {
|
||||||
|
return converter.parse(subtitleFile);
|
||||||
|
} catch (final ParseException e) {
|
||||||
|
throw new ExtractException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static double computeProgress(final CharSequence line, final long duration, final double oldProgress) {
|
private static double computeProgress(final CharSequence line, final long duration, final double oldProgress) {
|
||||||
final var matcher = LINE_PROGRESS_PATTERN.matcher(line);
|
final var matcher = LINE_PROGRESS_PATTERN.matcher(line);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
@@ -152,6 +159,11 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
|
|||||||
final var millis = Integer.parseInt(matcher.group("millis"));
|
final var millis = Integer.parseInt(matcher.group("millis"));
|
||||||
return ((minutes * 60L + seconds) * 1000 + millis) / (double) duration;
|
return ((minutes * 60L + seconds) * 1000 + millis) / (double) duration;
|
||||||
} else {
|
} else {
|
||||||
|
final var tqdmMatcher = TQDM_PROGRESS_PATTERN.matcher(line);
|
||||||
|
if (tqdmMatcher.find()) {
|
||||||
|
final var progress = Integer.parseInt(tqdmMatcher.group("progress"));
|
||||||
|
return progress / 100.0;
|
||||||
|
}
|
||||||
return oldProgress;
|
return oldProgress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,6 +173,8 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
|
|||||||
args.add(getPythonPath().toString());
|
args.add(getPythonPath().toString());
|
||||||
args.add("-m");
|
args.add("-m");
|
||||||
args.add("whisper");
|
args.add("whisper");
|
||||||
|
args.add("--verbose");
|
||||||
|
args.add("False");
|
||||||
args.add("--model");
|
args.add("--model");
|
||||||
if (model != WhisperModels.LARGE && language == Language.EN) {
|
if (model != WhisperModels.LARGE && language == Language.EN) {
|
||||||
args.add(model.name().toLowerCase() + ".en");
|
args.add(model.name().toLowerCase() + ".en");
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class JSONSubtitleConverter implements SubtitleConverter {
|
|||||||
return new SubtitleImpl(s.text(), start, end, null, null);
|
return new SubtitleImpl(s.text(), start, end, null, null);
|
||||||
}).sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList();
|
}).sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList();
|
||||||
final var language = Language.getLanguage(json.language());
|
final var language = Language.getLanguage(json.language());
|
||||||
final var subtitlesText = subtitles.stream().map(Subtitle::content).collect(Collectors.joining(" "));
|
final var subtitlesText = subtitles.stream().map(Subtitle::content).collect(Collectors.joining(""));
|
||||||
if (!Objects.equals(json.text(), subtitlesText)) {
|
if (!Objects.equals(json.text(), subtitlesText)) {
|
||||||
logger.warn("Not same text: {}\n\n{}", json.text(), subtitlesText);
|
logger.warn("Not same text: {}\n\n{}", json.text(), subtitlesText);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user