Extraction works

This commit is contained in:
Guillaume Tâche
2024-08-04 21:55:30 +02:00
parent 8002fc6719
commit 5efdaa6f63
121 changed files with 3360 additions and 400 deletions

View File

@@ -16,6 +16,11 @@
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-core</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 CondaBundledPath {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 CondaInstallerPath {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 CondaMinimumMajorVersion {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 CondaMinimumMinorVersion {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 CondaRootPath {
}

View File

@@ -0,0 +1,68 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
import com.github.gtache.autosubtitle.impl.OS;
import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
import java.net.http.HttpClient;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Setup module for Conda
*/
@Module
public abstract class CondaSetupModule {
private static final String CONDA = "conda";
private static final String MINICONDA3 = "miniconda3";
@Provides
@Singleton
static HttpClient providesHttpClient() {
return HttpClient.newHttpClient();
}
@Provides
@Singleton
@CondaSystemPath
static Path providesCondaSystemPath(final OS os) {
return Paths.get(os == OS.WINDOWS ? CONDA + ".bat" : CONDA);
}
@Provides
@Singleton
@CondaBundledPath
static Path providesCondaBundledPath(@CondaRootPath final Path root, final OS os) {
return root.resolve("condabin").resolve(Paths.get(os == OS.WINDOWS ? CONDA + ".bat" : CONDA));
}
@Provides
@Singleton
@CondaMinimumMajorVersion
static int providesCondaMinimumMajorVersion() {
return 24;
}
@Provides
@Singleton
@CondaMinimumMinorVersion
static int providesCondaMinimumMinorVersion() {
return 5;
}
@Provides
@Singleton
@CondaRootPath
static Path providesCondaRootPath(@WhisperBundledRoot final Path root, final OS os) {
return root.resolve(MINICONDA3);
}
@Provides
@Singleton
@CondaInstallerPath
static Path providesCondaInstallerPath(@WhisperBundledRoot final Path root, final OS os) {
return root.resolve("cache").resolve("conda-install" + (os == OS.WINDOWS ? ".exe" : ".sh"));
}
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 CondaSystemPath {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 PythonVersion {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 WhisperBundledRoot {
}

View File

@@ -0,0 +1,50 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
import com.github.gtache.autosubtitle.modules.setup.impl.SubtitleExtractorSetup;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.whisper.WhisperSetupManager;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Setup module for Whisper
*/
@Module(includes = CondaSetupModule.class)
public abstract class WhisperSetupModule {
@Binds
@SubtitleExtractorSetup
abstract SetupManager bindsSubtitleExtractorSetupManager(final WhisperSetupManager manager);
@Provides
@Singleton
@PythonVersion
static String providesPythonVersion() {
return "3.9.19";
}
@Provides
@Singleton
@WhisperVersion
static String providesWhisperVersion() {
return "20231117";
}
@Provides
@Singleton
@WhisperBundledRoot
static Path providesWhisperBundledRoot() {
return Paths.get("tools", "whisper");
}
@Provides
@Singleton
@WhisperVenvPath
static Path providesWhisperVenvPath(@WhisperBundledRoot final Path root) {
return root.resolve("whisper-env");
}
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 WhisperVenvPath {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
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 WhisperVersion {
}

View File

@@ -1,16 +1,22 @@
package com.github.gtache.autosubtitle.modules.whisper;
import com.github.gtache.autosubtitle.modules.whisper.json.JsonModule;
import com.github.gtache.autosubtitle.subtitle.ExtractionModelProvider;
import com.github.gtache.autosubtitle.subtitle.SubtitleExtractor;
import com.github.gtache.autosubtitle.whisper.WhisperExtractionModelProvider;
import com.github.gtache.autosubtitle.whisper.WhisperSubtitleExtractor;
import dagger.Binds;
import dagger.Module;
import javax.inject.Singleton;
@Module
public interface WhisperModule {
/**
* Dagger module for Whisper
*/
@Module(includes = JsonModule.class)
public abstract class WhisperModule {
@Binds
@Singleton
SubtitleExtractor bindsSubtitleExtractor(final WhisperSubtitleExtractor extractor);
abstract SubtitleExtractor bindsSubtitleExtractor(final WhisperSubtitleExtractor extractor);
@Binds
abstract ExtractionModelProvider bindsExtractionModelProvider(final WhisperExtractionModelProvider provider);
}

View File

@@ -0,0 +1,23 @@
package com.github.gtache.autosubtitle.modules.whisper.json;
import com.github.gtache.autosubtitle.whisper.SubtitleParser;
import com.github.gtache.autosubtitle.whisper.json.JsonSubtitleParser;
import com.google.gson.Gson;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
@Module
public abstract class JsonModule {
@Binds
abstract SubtitleParser bindsSubtitleParser(final JsonSubtitleParser subtitleParser);
@Provides
@Singleton
static Gson providesGson() {
return new Gson();
}
}

View File

@@ -1,19 +0,0 @@
package com.github.gtache.autosubtitle.setup.modules.whisper;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.modules.impl.SubtitleExtractorSetup;
import com.github.gtache.autosubtitle.setup.whisper.WhisperSetupManager;
import dagger.Binds;
import dagger.Module;
import javax.inject.Singleton;
@Module
public interface WhisperSetupModule {
@Binds
@Singleton
@SubtitleExtractorSetup
SetupManager bindsSubtitleExtractorSetupManager(final WhisperSetupManager manager);
}

View File

@@ -0,0 +1,260 @@
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.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.SetupStatus;
import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
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.Path;
import java.util.Arrays;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
/**
* Setup manager for Conda
*/
@Singleton
public class CondaSetupManager extends AbstractSetupManager {
private static final Logger logger = LogManager.getLogger(CondaSetupManager.class);
private final Path condaSystemPath;
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
CondaSetupManager(@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, 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
public String name() {
return "conda";
}
@Override
protected SetupStatus getStatus() throws SetupException {
if (isSystemCondaInstalled()) {
return SetupStatus.SYSTEM_INSTALLED;
} else if (isBundledCondaInstalled()) {
return SetupStatus.BUNDLE_INSTALLED;
} else {
return SetupStatus.NOT_INSTALLED;
}
}
@Override
public void install() throws SetupException {
if (!isSystemCondaInstalled() && !isBundledCondaInstalled()) {
installConda();
}
}
private void installConda() throws SetupException {
if (Files.exists(condaInstallerPath)) {
logger.info("Conda exists at {}", condaInstallerPath);
} else {
logger.info("Conda installer not found, downloading");
downloadConda();
logger.info("Conda downloaded");
}
switch (os) {
case WINDOWS -> installWindows();
case MAC, LINUX -> installLinux();
}
}
private void installLinux() throws SetupException {
try {
logger.info("Installing conda using {}", condaInstallerPath);
final var result = run("bash", condaInstallerPath.toString(), "-b", "-p", condaRootPath.toString());
if (result.exitCode() == 0) {
logger.info("Installed conda to {}", condaRootPath);
} else {
throw new SetupException("Error installing conda: " + result);
}
} catch (final IOException e) {
throw new SetupException(e);
}
}
private void installWindows() throws SetupException {
try {
logger.info("Installing conda using {}", condaInstallerPath);
final var result = run(condaInstallerPath.toString(), "/InstallationType=JustMe", "/RegisterPython=0", "/S", "/D=" + condaRootPath.toString());
if (result.exitCode() == 0) {
logger.info("Installed conda to {}", condaRootPath);
} else {
throw new SetupException("Error installing conda: " + result);
}
} catch (final IOException e) {
throw new SetupException(e);
}
}
private void downloadConda() throws SetupException {
switch (os) {
case WINDOWS -> downloadCondaWindows();
case MAC -> downloadCondaMac();
case LINUX -> downloadCondaLinux();
}
logger.info("Downloaded conda to {}", condaInstallerPath);
}
private void downloadCondaLinux() throws SetupException {
if (architecture.isAMD64()) {
downloadConda("https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh");
} else if (architecture.isARM64()) {
downloadConda("https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-aarch64.sh");
} else {
throw new SetupException("Unsupported architecture: " + architecture);
}
}
private void downloadCondaMac() throws SetupException {
if (architecture.isAMD64()) {
downloadConda("https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh");
} else if (architecture.isARM64()) {
downloadConda("https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh");
} else {
throw new SetupException("Unsupported architecture: " + architecture);
}
}
private void downloadCondaWindows() throws SetupException {
final var url = "https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe";
downloadConda(url);
}
private void downloadConda(final String url) throws SetupException {
final var request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
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
public void uninstall() throws SetupException {
deleteFolder(condaRootPath);
}
@Override
public void update() throws SetupException {
try {
final var result = run(getCondaPath().toString(), "update", "-y", "conda");
if (result.exitCode() == 0) {
logger.info("Conda updated");
} else {
throw new SetupException("Error updating conda: " + result.output());
}
} catch (final IOException e) {
throw new SetupException(e);
}
}
public void createVenv(final Path path, final String pythonVersion, final String... packages) throws SetupException {
final var args = Stream.concat(Stream.of(getCondaPath().toString(), "create", "-y", "-p", path.toString(), "python=" + pythonVersion), Arrays.stream(packages)).toList();
try {
logger.info("Creating venv {}", path);
final var result = run(args);
if (result.exitCode() == 0) {
logger.info("Created venv {}", path);
} else {
throw new SetupException("Error creating venv " + path + ": " + result.output());
}
// On Windows, we need to copy the DLLs otherwise pip may not work
if (os == OS.WINDOWS) {
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"))) {
final var fileList = files.toList();
final var targetFolder = path.resolve("DLLs");
for (final var s : fileList) {
Files.copy(s, targetFolder.resolve(s.getFileName()));
}
}
logger.info("Copied DLLs (Windows)");
}
} catch (final IOException e) {
throw new SetupException(e);
}
}
public boolean venvExists(final Path path) {
return Files.exists(path.resolve("bin").resolve(os == OS.WINDOWS ? "python.exe" : "python"));
}
private Path getCondaPath() throws SetupException {
return isSystemCondaInstalled() ? condaSystemPath : condaBundledPath;
}
private boolean isSystemCondaInstalled() throws SetupException {
try {
final var result = run(condaSystemPath.toString(), "--version");
if (result.exitCode() == 0) {
final var output = result.output().getFirst();
final var versionString = output.substring(output.indexOf(' ') + 1);
final var version = versionString.split("\\.");
if (version.length == 3) {
final var major = Integer.parseInt(version[0]);
final var minor = Integer.parseInt(version[1]);
return major >= condaMinimumMajorVersion && minor >= condaMinimumMinorVersion;
} else {
throw new SetupException("Unexpected python version: " + versionString);
}
} else {
return false;
}
} catch (final IOException e) {
throw new SetupException(e);
}
}
private boolean isBundledCondaInstalled() {
return Files.isRegularFile(condaBundledPath);
}
}

View File

@@ -1,32 +1,158 @@
package com.github.gtache.autosubtitle.setup.whisper;
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.SetupException;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.SetupStatus;
import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static java.util.Objects.requireNonNull;
@Singleton
public class WhisperSetupManager extends AbstractSetupManager {
private static final Logger logger = LogManager.getLogger(WhisperSetupManager.class);
private static final String CONDA_ENV = "conda-env";
private final CondaSetupManager condaSetupManager;
private final String pythonVersion;
private final Path whisperRoot;
private final Path venvPath;
private final OS os;
private final String whisperVersion;
@Inject
WhisperSetupManager(final CondaSetupManager condaSetupManager, @PythonVersion final String pythonVersion,
@WhisperBundledRoot final Path whisperRoot, @WhisperVenvPath final Path venvPath, final OS os,
@WhisperVersion final String whisperVersion) {
this.condaSetupManager = requireNonNull(condaSetupManager);
this.pythonVersion = requireNonNull(pythonVersion);
this.whisperRoot = requireNonNull(whisperRoot);
this.venvPath = requireNonNull(venvPath);
this.os = requireNonNull(os);
this.whisperVersion = requireNonNull(whisperVersion);
}
public class WhisperSetupManager implements SetupManager {
@Override
public String name() {
return "Whisper";
}
@Override
public SetupStatus status() {
return SetupStatus.NOT_INSTALLED;
protected SetupStatus getStatus() throws SetupException {
if (isWhisperInstalled()) {
return SetupStatus.BUNDLE_INSTALLED;
} else {
return SetupStatus.NOT_INSTALLED;
}
}
@Override
public void install() throws SetupException {
logger.info("Checking and installing conda");
checkInstallConda();
logger.info("Checking and creating venv");
checkInstallVenv();
logger.info("Checking and installing whisper");
checkInstallWhisper();
logger.info("Install finished");
}
private void checkInstallConda() throws SetupException {
sendStartEvent(SetupAction.CHECK, condaSetupManager.name(), 0);
if (condaSetupManager.isInstalled()) {
sendEndEvent(SetupAction.CHECK, condaSetupManager.name(), 0.33);
} else {
sendEndEvent(SetupAction.CHECK, condaSetupManager.name(), 0);
sendStartEvent(SetupAction.INSTALL, condaSetupManager.name(), -1);
condaSetupManager.install();
sendEndEvent(SetupAction.INSTALL, condaSetupManager.name(), 0.33);
}
}
private void checkInstallVenv() throws SetupException {
sendStartEvent(SetupAction.CHECK, CONDA_ENV, 0.33);
if (condaSetupManager.venvExists(venvPath)) {
sendEndEvent(SetupAction.CHECK, CONDA_ENV, 0.66);
} else {
sendEndEvent(SetupAction.CHECK, CONDA_ENV, -1);
sendStartEvent(SetupAction.INSTALL, CONDA_ENV, -1);
condaSetupManager.createVenv(venvPath, pythonVersion);
sendEndEvent(SetupAction.INSTALL, CONDA_ENV, 0.66);
}
}
private void checkInstallWhisper() throws SetupException {
sendStartEvent(SetupAction.CHECK, name(), 0.66);
if (isWhisperInstalled()) {
sendEndEvent(SetupAction.CHECK, name(), 1);
} else {
sendEndEvent(SetupAction.CHECK, name(), 0.66);
sendStartEvent(SetupAction.INSTALL, name(), -1);
installWhisper();
sendEndEvent(SetupAction.INSTALL, name(), 1);
}
}
@Override
public void uninstall() throws SetupException {
deleteFolder(whisperRoot);
}
@Override
public void update() throws SetupException {
sendStartEvent(SetupAction.CHECK, condaSetupManager.name(), 0);
if (condaSetupManager.isUpdateAvailable()) {
sendEndEvent(SetupAction.CHECK, condaSetupManager.name(), 0);
sendStartEvent(SetupAction.UPDATE, condaSetupManager.name(), -1);
condaSetupManager.update();
sendEndEvent(SetupAction.UPDATE, condaSetupManager.name(), 1);
} else {
sendEndEvent(SetupAction.CHECK, condaSetupManager.name(), 1);
}
}
private Path getPythonPath() {
return venvPath.resolve(os == OS.WINDOWS ? "python.exe" : "python");
}
private void installWhisper() throws SetupException {
final var path = getPythonPath();
try {
logger.info("Installing whisper");
final var result = run(path.toString(), "-m", "pip", "install", "-U", "openai-whisper==" + whisperVersion, "numpy<2");
if (result.exitCode() == 0) {
logger.info("Whisper installed");
} else {
throw new SetupException("Error installing whisper: " + result.output());
}
} catch (final IOException e) {
throw new SetupException(e);
}
}
private boolean isWhisperInstalled() throws SetupException {
final var path = getPythonPath();
if (Files.exists(path)) {
try {
final var result = run(path.toString(), "-m", "pip", "show", "openai-whisper");
return result.exitCode() == 0;
} catch (final IOException e) {
throw new SetupException(e);
}
} else {
return false;
}
}
}

View File

@@ -0,0 +1,13 @@
package com.github.gtache.autosubtitle.whisper;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import java.io.IOException;
import java.nio.file.Path;
public interface SubtitleParser {
SubtitleCollection parse(String text);
SubtitleCollection parse(Path file) throws IOException;
}

View File

@@ -0,0 +1,36 @@
package com.github.gtache.autosubtitle.whisper;
import com.github.gtache.autosubtitle.subtitle.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.ExtractionModelProvider;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.List;
/**
* Whisper implementation of {@link ExtractionModelProvider}
*/
@Singleton
public class WhisperExtractionModelProvider implements ExtractionModelProvider {
@Inject
WhisperExtractionModelProvider() {
}
@Override
public List<ExtractionModel> getAvailableExtractionModels() {
return Arrays.asList(WhisperModels.values());
}
@Override
public ExtractionModel getDefaultExtractionModel() {
return WhisperModels.MEDIUM;
}
@Override
public ExtractionModel getExtractionModel(final String name) {
return WhisperModels.valueOf(name.toUpperCase());
}
}

View File

@@ -0,0 +1,14 @@
package com.github.gtache.autosubtitle.whisper;
import com.github.gtache.autosubtitle.subtitle.ExtractionModel;
/**
* Whisper models
*/
public enum WhisperModels implements ExtractionModel {
TINY, BASE, SMALL, MEDIUM, LARGE;
public boolean hasEnglishSpecific() {
return this != LARGE;
}
}

View File

@@ -1,33 +1,175 @@
package com.github.gtache.autosubtitle.whisper;
import com.github.gtache.autosubtitle.Audio;
import com.github.gtache.autosubtitle.File;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner;
import com.github.gtache.autosubtitle.subtitle.ExtractEvent;
import com.github.gtache.autosubtitle.subtitle.ExtractException;
import com.github.gtache.autosubtitle.subtitle.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.SubtitleExtractorListener;
import com.github.gtache.autosubtitle.subtitle.impl.ExtractEventImpl;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collection;
import java.util.List;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import static java.util.Objects.requireNonNull;
/**
* Whisper implementation of {@link SubtitleExtractor}
*/
@Singleton
public class WhisperSubtitleExtractor implements SubtitleExtractor {
public class WhisperSubtitleExtractor extends AbstractProcessRunner implements SubtitleExtractor {
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 final Path venvPath;
private final SubtitleParser parser;
private final OS os;
private final Set<SubtitleExtractorListener> listeners;
@Inject
WhisperSubtitleExtractor() {
WhisperSubtitleExtractor(@WhisperVenvPath final Path venvPath, final SubtitleParser parser, final OS os) {
this.venvPath = requireNonNull(venvPath);
this.parser = requireNonNull(parser);
this.os = requireNonNull(os);
this.listeners = new HashSet<>();
}
@Override
public Collection<? extends EditableSubtitle> extract(final Video in) {
return List.of();
public void addListener(final SubtitleExtractorListener listener) {
listeners.add(listener);
}
@Override
public Collection<? extends EditableSubtitle> extract(final Audio in) {
return List.of();
public void removeListener(final SubtitleExtractorListener listener) {
listeners.remove(listener);
}
@Override
public void removeListeners() {
listeners.clear();
}
private void notifyListeners(final ExtractEvent event) {
listeners.forEach(listener -> listener.listen(event));
}
@Override
public SubtitleCollection extract(final Video video, final Language language, final ExtractionModel model) throws ExtractException {
if (video instanceof final File f) {
return extract(f.path(), language, model, video.info().duration());
} else {
try {
final var path = Files.createTempFile("autosubtitle", video.info().videoFormat());
try (final var in = video.getInputStream()) {
Files.copy(in, path);
final var ret = extract(path, language, model, video.info().duration());
Files.deleteIfExists(path);
return ret;
}
} catch (final IOException e) {
throw new ExtractException(e);
}
}
}
@Override
public SubtitleCollection extract(final Audio audio, final Language language, final ExtractionModel model) throws ExtractException {
if (audio instanceof final File f) {
return extract(f.path(), language, model, audio.info().duration());
} else {
try {
final var path = Files.createTempFile("autosubtitle", audio.info().audioFormat());
try (final var in = audio.getInputStream()) {
Files.copy(in, path);
final var ret = extract(path, language, model, audio.info().duration());
Files.deleteIfExists(path);
return ret;
}
} catch (final IOException e) {
throw new ExtractException(e);
}
}
}
private SubtitleCollection extract(final Path path, final Language language, final ExtractionModel model, final long duration) throws ExtractException {
try {
final var outputDir = Files.createTempDirectory("autosubtitle");
final var args = createArgs(path, language, model, outputDir);
final var processListener = startListen(args);
var line = processListener.readLine();
var oldProgress = -1.0;
while (line != null) {
final var newProgress = computeProgress(line, duration, oldProgress);
notifyListeners(new ExtractEventImpl(line, newProgress));
oldProgress = newProgress;
line = processListener.readLine();
}
final var result = processListener.join(Duration.ofHours(1));
if (result.exitCode() == 0) {
final var filename = path.getFileName().toString();
final var subtitleFilename = filename.substring(0, filename.lastIndexOf('.')) + ".json";
final var subtitleFile = outputDir.resolve(subtitleFilename);
return parser.parse(subtitleFile);
} else {
throw new ExtractException("Error extracting subtitles: " + result.output());
}
} catch (final IOException e) {
throw new ExtractException(e);
}
}
private double computeProgress(final CharSequence line, final long duration, final double oldProgress) {
final var matcher = LINE_PROGRESS_PATTERN.matcher(line);
if (matcher.find()) {
final var minutes = Integer.parseInt(matcher.group("minutes"));
final var seconds = Integer.parseInt(matcher.group("seconds"));
final var millis = Integer.parseInt(matcher.group("millis"));
return ((minutes * 60L + seconds) * 1000 + millis) / (double) duration;
} else {
return oldProgress;
}
}
private ArrayList<String> createArgs(final Path path, final Language language, final ExtractionModel model, final Path outputDir) {
final var args = new ArrayList<String>(14);
args.add(getPythonPath().toString());
args.add("-m");
args.add("whisper");
args.add("--model");
if (model != WhisperModels.LARGE && language == Language.EN) {
args.add(model.name().toLowerCase() + ".en");
} else {
args.add(model.name().toLowerCase());
}
args.add("--task");
args.add("transcribe");
args.add("--output_dir");
args.add(outputDir.toString());
args.add("--output_format");
args.add("json");
if (language != Language.AUTO) {
args.add("--language");
args.add(language.iso2());
}
args.add(path.toString());
return args;
}
private Path getPythonPath() {
return venvPath.resolve(os == OS.WINDOWS ? "python.exe" : "python");
}
}

View File

@@ -0,0 +1,8 @@
package com.github.gtache.autosubtitle.whisper.json;
import java.util.List;
public record JSONSubtitleSegment(int id, int seek, double start, double end, String text, List<Integer> tokens,
double temperature, double avg_logprob, double compression_ratio,
double no_speech_prob) {
}

View File

@@ -0,0 +1,6 @@
package com.github.gtache.autosubtitle.whisper.json;
import java.util.List;
public record JSONSubtitles(String text, List<JSONSubtitleSegment> segments, String language) {
}

View File

@@ -0,0 +1,54 @@
package com.github.gtache.autosubtitle.whisper.json;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import com.github.gtache.autosubtitle.whisper.SubtitleParser;
import com.google.gson.Gson;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.Objects;
import java.util.stream.Collectors;
@Singleton
public class JsonSubtitleParser implements SubtitleParser {
private static final Logger logger = LogManager.getLogger(JsonSubtitleParser.class);
private final Gson gson;
@Inject
JsonSubtitleParser(final Gson gson) {
this.gson = Objects.requireNonNull(gson);
}
@Override
public SubtitleCollection parse(final Path file) throws IOException {
final var content = Files.readString(file);
return parse(content);
}
@Override
public SubtitleCollection parse(final String text) {
final var json = gson.fromJson(text, JSONSubtitles.class);
final var subtitles = json.segments().stream().map(s -> {
final var start = (long) s.start() * 1000L;
final var end = (long) s.end() * 1000L;
return new SubtitleImpl(s.text(), start, end, null, null);
}).sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList();
final var language = Language.getLanguage(json.language());
final var subtitlesText = subtitles.stream().map(Subtitle::content).collect(Collectors.joining(" "));
if (!Objects.equals(json.text(), subtitlesText)) {
logger.warn("Not same text: {} vs {}", json.text(), subtitlesText);
}
return new SubtitleCollectionImpl(json.text(), subtitles, language);
}
}

View File

@@ -3,6 +3,15 @@
*/
module com.github.gtache.autosubtitle.whisper {
requires transitive com.github.gtache.autosubtitle.core;
requires transitive java.net.http;
requires org.apache.logging.log4j;
requires transitive com.google.gson;
requires transitive java.compiler; //Don't know why dagger generates @Generated here, need to debug
exports com.github.gtache.autosubtitle.whisper;
exports com.github.gtache.autosubtitle.whisper.json;
exports com.github.gtache.autosubtitle.setup.whisper;
exports com.github.gtache.autosubtitle.modules.whisper;
exports com.github.gtache.autosubtitle.modules.whisper.json;
exports com.github.gtache.autosubtitle.modules.setup.whisper;
}