Adds WhisperX, reworks UI (still needs some work), theoretically usable

This commit is contained in:
Guillaume Tâche
2024-08-17 22:05:04 +02:00
parent 7bddf53bab
commit 3fa51eb95b
204 changed files with 4787 additions and 1321 deletions

19
whisper/base/pom.xml Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>autosubtitle-whisper-base</artifactId>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,19 +1,23 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
package com.github.gtache.autosubtitle.modules.setup.whisper.base;
import com.github.gtache.autosubtitle.modules.setup.impl.SubtitleExtractorSetup;
import com.github.gtache.autosubtitle.modules.setup.impl.ToolsRoot;
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.WhisperCommonSetupModule;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.whisper.WhisperSetupManager;
import com.github.gtache.autosubtitle.setup.whisper.base.WhisperSetupManager;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Setup module for Whisper
*/
@Module(includes = CondaSetupModule.class)
@Module(includes = WhisperCommonSetupModule.class)
public abstract class WhisperSetupModule {
@Binds
@SubtitleExtractorSetup
@@ -25,16 +29,10 @@ public abstract class WhisperSetupModule {
return "3.9.19";
}
@Provides
@WhisperVersion
static String providesWhisperVersion() {
return "20231117";
}
@Provides
@WhisperBundledRoot
static Path providesWhisperBundledRoot() {
return Paths.get("tools", "whisper");
static Path providesWhisperBundledRoot(@ToolsRoot final Path root) {
return root.resolve("whisper");
}
@Provides

View File

@@ -1,7 +1,7 @@
package com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper;
package com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper.base;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.whisper.WhisperSubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.whisper.base.WhisperSubtitleExtractor;
import dagger.Binds;
import dagger.Module;

View File

@@ -1,7 +1,7 @@
package com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisper;
package com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisper.base;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.parser.json.whisper.JSONSubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.parser.json.whisper.base.JSONSubtitleConverter;
import com.google.gson.Gson;
import dagger.Binds;
import dagger.Module;

View File

@@ -1,8 +1,8 @@
package com.github.gtache.autosubtitle.modules.whisper;
package com.github.gtache.autosubtitle.modules.whisper.base;
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.parser.json.whisper.WhisperJsonModule;
import com.github.gtache.autosubtitle.modules.setup.whisper.base.WhisperSetupModule;
import com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper.base.WhisperExtractorModule;
import com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisper.base.WhisperJsonModule;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
import com.github.gtache.autosubtitle.whisper.WhisperExtractionModelProvider;
import dagger.Binds;

View File

@@ -0,0 +1,65 @@
package com.github.gtache.autosubtitle.setup.whisper.base;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.conda.CondaSetupManager;
import com.github.gtache.autosubtitle.setup.whisper.AbstractWhisperSetupManager;
import com.github.gtache.autosubtitle.setup.whisper.WhisperSetupConfiguration;
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.time.Duration;
import java.util.List;
/**
* {@link com.github.gtache.autosubtitle.setup.SetupManager} for Whisper
*/
@Singleton
public class WhisperSetupManager extends AbstractWhisperSetupManager {
private static final Logger logger = LogManager.getLogger(WhisperSetupManager.class);
@Inject
WhisperSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration) {
super(condaSetupManager, configuration);
}
@Override
public String name() {
return "Whisper";
}
@Override
protected void installWhisper() throws SetupException {
final var path = getPythonPath();
try {
logger.info("Installing whisper");
final var result = run(List.of(path.toString(), "-m", "pip", "install", "-U", "openai-whisper", "numpy<2"), Duration.ofMinutes(15));
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);
}
}
@Override
protected boolean isWhisperInstalled() throws SetupException {
final var path = getPythonPath();
if (Files.exists(path)) {
try {
final var result = run(List.of(path.toString(), "-m", "pip", "show", "openai-whisper"), Duration.ofSeconds(5));
return result.exitCode() == 0;
} catch (final IOException e) {
throw new SetupException(e);
}
} else {
return false;
}
}
}

View File

@@ -0,0 +1,58 @@
package com.github.gtache.autosubtitle.subtitle.extractor.whisper.base;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.whisper.AbstractWhisperSubtitleExtractor;
import com.github.gtache.autosubtitle.whisper.WhisperModels;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Whisper implementation of {@link SubtitleExtractor}
*/
@Singleton
public class WhisperSubtitleExtractor extends AbstractWhisperSubtitleExtractor {
@Inject
WhisperSubtitleExtractor(@WhisperVenvPath final Path venvPath, final Map<String, SubtitleConverter> converters, final OS os) {
super(venvPath, converters, os);
}
@Override
protected List<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("--verbose");
args.add("False");
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;
}
}

View File

@@ -1,4 +1,4 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisper;
package com.github.gtache.autosubtitle.subtitle.parser.json.whisper.base;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
@@ -24,7 +24,7 @@ import java.util.stream.Collectors;
* {@link SubtitleConverter} implementation for JSON files
*/
@Singleton
public class JSONSubtitleConverter implements SubtitleConverter {
public class JSONSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
private static final Logger logger = LogManager.getLogger(JSONSubtitleConverter.class);
private final Gson gson;
@@ -35,7 +35,7 @@ public class JSONSubtitleConverter implements SubtitleConverter {
}
@Override
public String format(final SubtitleCollection collection) {
public String format(final SubtitleCollection<?> collection) {
final var id = new AtomicInteger(0);
final var segments = collection.subtitles().stream().map(s -> new JSONSubtitleSegment(id.incrementAndGet(), 0, s.start() / (double) 1000,
s.end() / (double) 1000, s.content(), List.of(), 0, 0, 0, 0)).toList();
@@ -44,7 +44,7 @@ public class JSONSubtitleConverter implements SubtitleConverter {
}
@Override
public SubtitleCollection parse(final String content) throws ParseException {
public SubtitleCollectionImpl<SubtitleImpl> parse(final String content) throws ParseException {
try {
final var json = gson.fromJson(content, JSONSubtitles.class);
final var subtitles = json.segments().stream().map(s -> {
@@ -57,7 +57,7 @@ public class JSONSubtitleConverter implements SubtitleConverter {
if (!Objects.equals(json.text(), subtitlesText)) {
logger.warn("Not same text: {}\n\n{}", json.text(), subtitlesText);
}
return new SubtitleCollectionImpl(json.text(), subtitles, language);
return new SubtitleCollectionImpl<>(json.text(), subtitles, language);
} catch (final Exception e) {
throw new ParseException(e);
}

View File

@@ -1,4 +1,4 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisper;
package com.github.gtache.autosubtitle.subtitle.parser.json.whisper.base;
import java.util.List;

View File

@@ -1,4 +1,4 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisper;
package com.github.gtache.autosubtitle.subtitle.parser.json.whisper.base;
import java.util.List;

View File

@@ -0,0 +1,15 @@
/**
* Base whisper implementation (openai-whisper)
*/
module com.github.gtache.autosubtitle.whisper.base {
requires transitive com.github.gtache.autosubtitle.whisper.common;
requires com.github.gtache.autosubtitle.core;
requires org.apache.logging.log4j;
requires com.google.gson;
exports com.github.gtache.autosubtitle.setup.whisper.base;
exports com.github.gtache.autosubtitle.modules.whisper.base;
exports com.github.gtache.autosubtitle.modules.setup.whisper.base;
exports com.github.gtache.autosubtitle.subtitle.extractor.whisper.base;
exports com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper.base;
}

21
whisper/common/pom.xml Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>autosubtitle-whisper-common</artifactId>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-conda</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,26 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.setup.conda.CondaSetupModule;
import com.github.gtache.autosubtitle.setup.whisper.WhisperSetupConfiguration;
import dagger.Module;
import dagger.Provides;
import java.nio.file.Path;
@Module(includes = CondaSetupModule.class)
public final class WhisperCommonSetupModule {
private WhisperCommonSetupModule() {
}
@Provides
static WhisperSetupConfiguration providesWhisperSetupConfiguration(@WhisperBundledRoot final Path root,
@WhisperVenvPath final Path venvPath,
@PythonVersion final String pythonVersion,
final OS os) {
return new WhisperSetupConfiguration(root, venvPath, pythonVersion, os);
}
}

View File

@@ -3,15 +3,13 @@ package com.github.gtache.autosubtitle.setup.whisper;
import com.github.gtache.autosubtitle.impl.OS;
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.conda.CondaSetupManager;
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;
@@ -19,27 +17,20 @@ import static java.util.Objects.requireNonNull;
//TODO add gpg/signature check
/**
* Setup manager for Whisper
* Base {@link SetupManager} for Whisper
*/
@Singleton
public class WhisperSetupManager extends AbstractSetupManager {
public abstract class AbstractWhisperSetupManager extends AbstractSetupManager {
private static final Logger logger = LogManager.getLogger(WhisperSetupManager.class);
private static final Logger logger = LogManager.getLogger(AbstractWhisperSetupManager.class);
private static final String CONDA_ENV = "conda-env";
private final CondaSetupManager condaSetupManager;
private final WhisperSetupConfiguration configuration;
@Inject
WhisperSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration) {
protected AbstractWhisperSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration) {
this.condaSetupManager = requireNonNull(condaSetupManager);
this.configuration = requireNonNull(configuration);
}
@Override
public String name() {
return "Whisper";
}
@Override
protected SetupStatus getStatus() throws SetupException {
if (isWhisperInstalled()) {
@@ -114,38 +105,25 @@ public class WhisperSetupManager extends AbstractSetupManager {
}
}
private Path getPythonPath() {
/**
* @return the path to the python executable
*/
protected Path getPythonPath() {
return configuration.venvPath().resolve(configuration.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==" + configuration.whisperVersion(), "numpy<2");
if (result.exitCode() == 0) {
logger.info("Whisper installed");
} else {
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) {
throw new SetupException(e);
}
}
/**
* Installs Whisper
*
* @throws SetupException if an error occurred
*/
protected abstract void installWhisper() throws SetupException;
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;
}
}
/**
* Check if whisper is installed
*
* @return true if whisper is installed
* @throws SetupException
*/
protected abstract boolean isWhisperInstalled() throws SetupException;
}

View File

@@ -8,14 +8,12 @@ import java.util.Objects;
/**
* Configuration for whisper setup
*/
public record WhisperSetupConfiguration(Path root, Path venvPath, String pythonVersion,
String whisperVersion, OS os) {
public record WhisperSetupConfiguration(Path root, Path venvPath, String pythonVersion, OS os) {
public WhisperSetupConfiguration {
Objects.requireNonNull(root);
Objects.requireNonNull(venvPath);
Objects.requireNonNull(pythonVersion);
Objects.requireNonNull(whisperVersion);
Objects.requireNonNull(os);
}
}

View File

@@ -5,8 +5,8 @@ import com.github.gtache.autosubtitle.File;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
@@ -16,18 +16,15 @@ import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractorListener;
import com.github.gtache.autosubtitle.subtitle.extractor.impl.ExtractEventImpl;
import com.github.gtache.autosubtitle.whisper.WhisperModels;
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.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
@@ -35,23 +32,22 @@ import java.util.regex.Pattern;
import static java.util.Objects.requireNonNull;
/**
* Whisper implementation of {@link SubtitleExtractor}
* Base implementation of {@link SubtitleExtractor} for Whisper
*/
@Singleton
public class WhisperSubtitleExtractor extends AbstractProcessRunner implements SubtitleExtractor {
public abstract class AbstractWhisperSubtitleExtractor extends AbstractProcessRunner implements SubtitleExtractor {
private static final Logger logger = LogManager.getLogger(WhisperSubtitleExtractor.class);
private static final Logger logger = LogManager.getLogger(AbstractWhisperSubtitleExtractor.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 TQDM_PROGRESS_PATTERN = Pattern.compile("^(?<progress>\\d{1,3})%|.+");
private static final Pattern PROGRESS_PATTERN = Pattern.compile("^Progress:\\s*(?<progress>\\d{1,3}\\.\\d{1,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 SubtitleConverter converter;
private final OS os;
private final Set<SubtitleExtractorListener> listeners;
@Inject
WhisperSubtitleExtractor(@WhisperVenvPath final Path venvPath, final Map<String, SubtitleConverter> converters, final OS os) {
protected AbstractWhisperSubtitleExtractor(final Path venvPath, final Map<String, SubtitleConverter> converters, final OS os) {
this.venvPath = requireNonNull(venvPath);
this.converter = requireNonNull(converters.get("json"));
this.os = requireNonNull(os);
@@ -78,7 +74,7 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
}
@Override
public SubtitleCollection extract(final Video video, final Language language, final ExtractionModel model) throws ExtractException {
public SubtitleCollection<Subtitle> 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 {
@@ -97,7 +93,7 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
}
@Override
public SubtitleCollection extract(final Audio audio, final Language language, final ExtractionModel model) throws ExtractException {
public SubtitleCollection<Subtitle> 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 {
@@ -115,7 +111,7 @@ 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<Subtitle> 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);
@@ -124,7 +120,7 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
var line = processListener.readLine();
while (line != null) {
logger.info("[whisper]: {}", line);
final var newProgress = computeProgress(line, duration, oldProgress);
final var newProgress = computeProgress(line, oldProgress, duration);
notifyListeners(new ExtractEventImpl(line, newProgress));
oldProgress = newProgress;
line = processListener.readLine();
@@ -143,7 +139,7 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
}
}
private SubtitleCollection parseResult(final Path subtitleFile) throws ExtractException {
private SubtitleCollection<Subtitle> parseResult(final Path subtitleFile) throws ExtractException {
try {
return converter.parse(subtitleFile);
} catch (final ParseException e) {
@@ -151,51 +147,46 @@ public class WhisperSubtitleExtractor extends AbstractProcessRunner implements S
}
}
private static double computeProgress(final CharSequence line, final long duration, final double oldProgress) {
private static double computeProgress(final CharSequence line, final double oldProgress, final long duration) {
final var matcher = LINE_PROGRESS_PATTERN.matcher(line);
if (matcher.find()) {
if (matcher.matches()) {
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 {
final var tqdmMatcher = TQDM_PROGRESS_PATTERN.matcher(line);
if (tqdmMatcher.find()) {
final var progress = Integer.parseInt(tqdmMatcher.group("progress"));
final var progressMatcher = PROGRESS_PATTERN.matcher(line);
if (progressMatcher.matches()) {
final var progress = Double.parseDouble(progressMatcher.group("progress"));
return progress / 100.0;
} else {
final var tqdmMatcher = TQDM_PROGRESS_PATTERN.matcher(line);
if (tqdmMatcher.matches()) {
final var progress = Integer.parseInt(tqdmMatcher.group("progress"));
return progress / 100.0;
} else {
return oldProgress;
}
}
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("--verbose");
args.add("False");
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;
}
/**
* Creates the command line arguments for Whisper
*
* @param path the path to the file
* @param language the language
* @param model the model
* @param outputDir the output directory
* @return the list of arguments
*/
protected abstract List<String> createArgs(final Path path, final Language language, final ExtractionModel model, final Path outputDir);
private Path getPythonPath() {
/**
* @return the path to the python executable
*/
protected Path getPythonPath() {
return venvPath.resolve(os == OS.WINDOWS ? "python.exe" : "python");
}
}

View File

@@ -1,8 +1,8 @@
/**
* Whisper module for auto-subtitle
* Common whisper module
*/
module com.github.gtache.autosubtitle.whisper {
requires transitive com.github.gtache.autosubtitle.core;
module com.github.gtache.autosubtitle.whisper.common {
requires transitive com.github.gtache.autosubtitle.conda;
requires transitive java.net.http;
requires org.apache.logging.log4j;
requires transitive com.google.gson;
@@ -10,10 +10,6 @@ module com.github.gtache.autosubtitle.whisper {
exports com.github.gtache.autosubtitle.whisper;
exports com.github.gtache.autosubtitle.setup.whisper;
exports com.github.gtache.autosubtitle.modules.whisper;
exports com.github.gtache.autosubtitle.modules.setup.whisper;
exports com.github.gtache.autosubtitle.subtitle.extractor.whisper;
exports com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper;
exports com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisper;
exports com.github.gtache.autosubtitle.subtitle.parser.json.whisper;
exports com.github.gtache.autosubtitle.modules.setup.whisper;
}

View File

@@ -10,16 +10,42 @@
</parent>
<artifactId>autosubtitle-whisper</artifactId>
<packaging>pom</packaging>
<modules>
<module>base</module>
<module>common</module>
<module>whisperx</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisperx</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-core</artifactId>
<artifactId>autosubtitle-conda</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>

View File

@@ -1,16 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,68 +0,0 @@
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.setup.whisper.CondaSetupConfiguration;
import dagger.Module;
import dagger.Provides;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Setup module for Conda
*/
@Module
public final class CondaSetupModule {
private static final String CONDA = "conda";
private static final String MINICONDA3 = "miniconda3";
private CondaSetupModule() {
}
@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
@CondaSystemPath
static Path providesCondaSystemPath(final OS os) {
return Paths.get(os == OS.WINDOWS ? CONDA + ".bat" : CONDA);
}
@Provides
@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
@CondaMinimumMajorVersion
static int providesCondaMinimumMajorVersion() {
return 24;
}
@Provides
@CondaMinimumMinorVersion
static int providesCondaMinimumMinorVersion() {
return 5;
}
@Provides
@CondaRootPath
static Path providesCondaRootPath(@WhisperBundledRoot final Path root, final OS os) {
return root.resolve(MINICONDA3);
}
@Provides
@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

@@ -1,16 +0,0 @@
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

@@ -1,16 +0,0 @@
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,30 +0,0 @@
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);
}
}

View File

@@ -1,226 +0,0 @@
package com.github.gtache.autosubtitle.setup.whisper;
import com.github.gtache.autosubtitle.impl.OS;
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.http.HttpClient;
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 CondaSetupConfiguration configuration;
@Inject
CondaSetupManager(final CondaSetupConfiguration configuration, final HttpClient httpClient) {
super(httpClient);
this.configuration = requireNonNull(configuration);
}
@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 {
final var installerPath = configuration.condaInstallerPath();
if (Files.exists(installerPath)) {
logger.info("Conda exists at {}", installerPath);
} else {
logger.info("Conda installer not found, downloading");
downloadConda();
logger.info("Conda downloaded");
}
switch (configuration.os()) {
case WINDOWS -> installWindows();
case MAC, LINUX -> installLinux();
}
}
private void installLinux() throws SetupException {
try {
final var installerPath = configuration.condaInstallerPath();
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) {
logger.info("Installed conda to {}", rootPath);
} else {
throw new SetupException("Error installing conda: " + result);
}
} catch (final IOException e) {
throw new SetupException(e);
}
}
private void installWindows() throws SetupException {
try {
final var installerPath = configuration.condaInstallerPath();
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) {
logger.info("Installed conda to {}", rootPath);
} else {
throw new SetupException("Error installing conda: " + result);
}
} catch (final IOException e) {
throw new SetupException(e);
}
}
private void downloadConda() throws SetupException {
switch (configuration.os()) {
case WINDOWS -> downloadCondaWindows();
case MAC -> downloadCondaMac();
case LINUX -> downloadCondaLinux();
}
logger.info("Downloaded conda to {}", configuration.condaInstallerPath());
}
private void downloadCondaLinux() throws SetupException {
final var architecture = configuration.architecture();
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 {
final var architecture = configuration.architecture();
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 {
download(url, configuration.condaInstallerPath());
}
@Override
public void uninstall() throws SetupException {
deleteFolder(configuration.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 (configuration.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(configuration.os() == OS.WINDOWS ? "python.exe" : "python"));
}
private Path getCondaPath() throws SetupException {
return isSystemCondaInstalled() ? configuration.condaSystemPath() : configuration.condaBundledPath();
}
private boolean isSystemCondaInstalled() throws SetupException {
try {
final var result = run(configuration.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 >= configuration.condaMinimumMajorVersion() && minor >= configuration.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(configuration.condaBundledPath());
}
}

20
whisper/whisperx/pom.xml Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>autosubtitle-whisperx</artifactId>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,43 @@
package com.github.gtache.autosubtitle.modules.setup.whisperx;
import com.github.gtache.autosubtitle.modules.setup.impl.SubtitleExtractorSetup;
import com.github.gtache.autosubtitle.modules.setup.impl.ToolsRoot;
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.WhisperCommonSetupModule;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.whisperx.WhisperXSetupManager;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import java.nio.file.Path;
/**
* Setup module for Whisper
*/
@Module(includes = WhisperCommonSetupModule.class)
public abstract class WhisperXSetupModule {
@Binds
@SubtitleExtractorSetup
abstract SetupManager bindsSubtitleExtractorSetupManager(final WhisperXSetupManager manager);
@Provides
@PythonVersion
static String providesPythonVersion() {
return "3.10";
}
@Provides
@WhisperBundledRoot
static Path providesWhisperXBundledRoot(@ToolsRoot final Path root) {
return root.resolve("whisperx");
}
@Provides
@WhisperVenvPath
static Path providesWhisperXVenvPath(@WhisperBundledRoot final Path root) {
return root.resolve("whisperx-env");
}
}

View File

@@ -0,0 +1,20 @@
package com.github.gtache.autosubtitle.modules.subtitle.extractor.whisperx;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.whisperx.WhisperXSubtitleExtractor;
import dagger.Binds;
import dagger.Module;
/**
* Dagger module for Whisper
*/
@Module
public abstract class WhisperXExtractorModule {
private WhisperXExtractorModule() {
}
@Binds
abstract SubtitleExtractor bindsSubtitleExtractor(final WhisperXSubtitleExtractor extractor);
}

View File

@@ -0,0 +1,31 @@
package com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisperx;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.parser.json.whisperx.JSONSubtitleConverter;
import com.google.gson.Gson;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import javax.inject.Singleton;
@Module
public abstract class WhisperXJsonModule {
private WhisperXJsonModule() {
}
@Binds
@IntoMap
@StringKey("json")
abstract SubtitleConverter bindsJSONSubtitleConverter(final JSONSubtitleConverter subtitleConverter);
@Provides
@Singleton
static Gson providesGson() {
return new Gson();
}
}

View File

@@ -0,0 +1,23 @@
package com.github.gtache.autosubtitle.modules.whisperx;
import com.github.gtache.autosubtitle.modules.setup.whisperx.WhisperXSetupModule;
import com.github.gtache.autosubtitle.modules.subtitle.extractor.whisperx.WhisperXExtractorModule;
import com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisperx.WhisperXJsonModule;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModelProvider;
import com.github.gtache.autosubtitle.whisperx.WhisperXExtractionModelProvider;
import dagger.Binds;
import dagger.Module;
/**
* Dagger module for Whisper
*/
@Module(includes = {WhisperXSetupModule.class, WhisperXJsonModule.class, WhisperXExtractorModule.class})
public abstract class WhisperXModule {
private WhisperXModule() {
}
@Binds
abstract ExtractionModelProvider bindsExtractionModelProvider(final WhisperXExtractionModelProvider provider);
}

View File

@@ -0,0 +1,65 @@
package com.github.gtache.autosubtitle.setup.whisperx;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.conda.CondaSetupManager;
import com.github.gtache.autosubtitle.setup.whisper.AbstractWhisperSetupManager;
import com.github.gtache.autosubtitle.setup.whisper.WhisperSetupConfiguration;
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.time.Duration;
import java.util.List;
/**
* {@link com.github.gtache.autosubtitle.setup.SetupManager} for WhisperX
*/
@Singleton
public class WhisperXSetupManager extends AbstractWhisperSetupManager {
private static final Logger logger = LogManager.getLogger(WhisperXSetupManager.class);
@Inject
WhisperXSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration) {
super(condaSetupManager, configuration);
}
@Override
public String name() {
return "WhisperX";
}
@Override
protected void installWhisper() throws SetupException {
final var path = getPythonPath();
try {
logger.info("Installing whisper");
final var result = run(List.of(path.toString(), "-m", "pip", "install", "-U", "git+https://github.com/m-bain/whisperx.git", "numpy<2"), Duration.ofMinutes(15));
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);
}
}
@Override
protected boolean isWhisperInstalled() throws SetupException {
final var path = getPythonPath();
if (Files.exists(path)) {
try {
final var result = run(List.of(path.toString(), "-m", "pip", "show", "whisperx"), Duration.ofSeconds(5));
return result.exitCode() == 0;
} catch (final IOException e) {
throw new SetupException(e);
}
} else {
return false;
}
}
}

View File

@@ -0,0 +1,69 @@
package com.github.gtache.autosubtitle.subtitle.extractor.whisperx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.extractor.whisper.AbstractWhisperSubtitleExtractor;
import com.github.gtache.autosubtitle.whisper.WhisperModels;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* WhisperX implementation of {@link SubtitleExtractor}
*/
@Singleton
public class WhisperXSubtitleExtractor extends AbstractWhisperSubtitleExtractor {
@Inject
WhisperXSubtitleExtractor(@WhisperVenvPath final Path venvPath, final Map<String, SubtitleConverter> converters, final OS os) {
super(venvPath, converters, os);
}
@Override
protected List<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("whisperx");
args.add("--verbose");
args.add("False");
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");
args.add("--device");
args.add("cpu");
args.add("--condition_on_previous_text");
args.add("True");
args.add("--print_progress");
args.add("True");
args.add("--compute_type");
args.add("int8");
args.add("--max_line_count");
args.add("2");
args.add("--max_line_width");
args.add("30");
if (language != Language.AUTO) {
args.add("--language");
args.add(language.iso2());
}
args.add(path.toString());
return args;
}
}

View File

@@ -0,0 +1,67 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisperx;
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.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import com.google.gson.Gson;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* {@link SubtitleConverter} implementation for JSON files
*/
@Singleton
public class JSONSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
private final Gson gson;
@Inject
JSONSubtitleConverter(final Gson gson) {
this.gson = Objects.requireNonNull(gson);
}
@Override
public String format(final SubtitleCollection<?> collection) {
final var segments = collection.subtitles().stream().map(s -> new JSONSubtitleSegment(s.start() / (double) 1000,
s.end() / (double) 1000, s.content(), List.of())).toList();
final var subtitles = new JSONSubtitles(segments, collection.language().iso2());
return gson.toJson(subtitles);
}
@Override
public SubtitleCollectionImpl<SubtitleImpl> parse(final String content) throws ParseException {
try {
final var json = gson.fromJson(content, JSONSubtitles.class);
final var subtitles = json.segments().stream().map(s -> {
final var start = (long) s.start() * 1000L;
final var end = (long) s.end() * 1000L;
return new SubtitleImpl(s.text(), start, end, null, null);
}).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(""));
return new SubtitleCollectionImpl<>(subtitlesText, subtitles, language);
} catch (final Exception e) {
throw new ParseException(e);
}
}
@Override
public boolean canParse(final Path file) {
return file.getFileName().toString().endsWith(".json");
}
@Override
public String formatName() {
return "json";
}
}

View File

@@ -0,0 +1,6 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisperx;
import java.util.List;
public record JSONSubtitleSegment(double start, double end, String text, List<JSONSubtitleWords> words) {
}

View File

@@ -0,0 +1,4 @@
package com.github.gtache.autosubtitle.subtitle.parser.json.whisperx;
public record JSONSubtitleWords(String word, double start, double end, double score) {
}

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
/**
* WhisperX module for auto-subtitle
*/
module com.github.gtache.autosubtitle.whisperx {
requires transitive com.github.gtache.autosubtitle.whisper.common;
requires org.apache.logging.log4j;
exports com.github.gtache.autosubtitle.whisperx;
exports com.github.gtache.autosubtitle.setup.whisperx;
exports com.github.gtache.autosubtitle.modules.whisperx;
exports com.github.gtache.autosubtitle.modules.setup.whisperx;
exports com.github.gtache.autosubtitle.subtitle.extractor.whisperx;
exports com.github.gtache.autosubtitle.modules.subtitle.extractor.whisperx;
exports com.github.gtache.autosubtitle.modules.subtitle.parser.json.whisperx;
exports com.github.gtache.autosubtitle.subtitle.parser.json.whisperx;
}