WIP: Submit transcoding jobs via a HTTP API #6
@ -210,6 +210,33 @@ public class JDBCTranscriptionRepository implements TranscriptionRepository {
|
|||||||
.update();
|
.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<SourceFile> getFile(Transcription transcription, UUID fileId) {
|
||||||
|
return jdbcClient.sql("""
|
||||||
|
SELECT id, filename, transcribed_result_uri
|
||||||
|
FROM transcriptions_files
|
||||||
|
WHERE transcription_id = :transcription_id
|
||||||
|
AND id = :file_id
|
||||||
|
""")
|
||||||
|
.param("transcription_id", transcription.id())
|
||||||
|
.param("file_id", fileId)
|
||||||
|
.query(SourceFileRowMapper.INSTANCE)
|
||||||
|
.optional();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Job> findJobBySourceFile(SourceFile sourceFile) {
|
||||||
|
return jdbcClient.sql("""
|
||||||
|
SELECT jobs.id as jobId, result_file_absolute_path, error_message, tf.id, tf.filename, tf.transcribed_result_uri
|
||||||
|
FROM jobs
|
||||||
|
INNER JOIN transcriptions_files tf on jobs.source_file_id = tf.id
|
||||||
|
WHERE jobs.source_file_id = :source_file_id
|
||||||
|
""")
|
||||||
|
.param("source_file_id", sourceFile.id())
|
||||||
|
.query(JobRowMapper.INSTANCE)
|
||||||
|
.optional();
|
||||||
|
}
|
||||||
|
|
||||||
private enum TranscriptionRowMapper implements RowMapper<Transcription> {
|
private enum TranscriptionRowMapper implements RowMapper<Transcription> {
|
||||||
INSTANCE;
|
INSTANCE;
|
||||||
|
|
||||||
|
@ -30,7 +30,15 @@ public class WhisperApiApplication {
|
|||||||
.authorizeHttpRequests(authorize -> authorize
|
.authorizeHttpRequests(authorize -> authorize
|
||||||
.requestMatchers("/api/transcriptions/job/callback/**").permitAll()
|
.requestMatchers("/api/transcriptions/job/callback/**").permitAll()
|
||||||
.anyRequest().authenticated())
|
.anyRequest().authenticated())
|
||||||
.httpBasic(Customizer.withDefaults())
|
.httpBasic(basic -> basic.authenticationEntryPoint((request, response, authException) -> {
|
||||||
|
// Override the entry point because the default one uses
|
||||||
|
// HttpServletResponse#sendError method which causes a redirect to /error
|
||||||
|
// which will in turn be caught by the OAuth2 filter that will further redirect
|
||||||
|
// to the login page.
|
||||||
|
// We want the client to stay at the right URL and only be prompted for credentials.
|
||||||
|
response.setStatus(401);
|
||||||
|
response.setHeader("WWW-Authenticate", "Basic realm=\"whisper\"");
|
||||||
|
}))
|
||||||
.csrf(csrf -> csrf.disable())
|
.csrf(csrf -> csrf.disable())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package se.su.dsv.whisperapi.api;
|
package se.su.dsv.whisperapi.api;
|
||||||
|
|
||||||
|
import org.springframework.core.io.InputStreamResource;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.ErrorResponseException;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -15,9 +18,14 @@ import se.su.dsv.whisperapi.core.CreateTranscription;
|
|||||||
import se.su.dsv.whisperapi.core.JobAlreadyCompleted;
|
import se.su.dsv.whisperapi.core.JobAlreadyCompleted;
|
||||||
import se.su.dsv.whisperapi.core.JobCompletion;
|
import se.su.dsv.whisperapi.core.JobCompletion;
|
||||||
import se.su.dsv.whisperapi.core.JobNotFound;
|
import se.su.dsv.whisperapi.core.JobNotFound;
|
||||||
|
import se.su.dsv.whisperapi.core.NoSuchSourceFile;
|
||||||
|
import se.su.dsv.whisperapi.core.NotSubmittedForTranscription;
|
||||||
import se.su.dsv.whisperapi.core.OutputFormat;
|
import se.su.dsv.whisperapi.core.OutputFormat;
|
||||||
|
import se.su.dsv.whisperapi.core.TranscribedResult;
|
||||||
import se.su.dsv.whisperapi.core.Transcription;
|
import se.su.dsv.whisperapi.core.Transcription;
|
||||||
import se.su.dsv.whisperapi.core.SourceFileUpload;
|
import se.su.dsv.whisperapi.core.SourceFileUpload;
|
||||||
|
import se.su.dsv.whisperapi.core.TranscriptionJobFailed;
|
||||||
|
import se.su.dsv.whisperapi.core.TranscriptionJobStillPending;
|
||||||
import se.su.dsv.whisperapi.core.TranscriptionService;
|
import se.su.dsv.whisperapi.core.TranscriptionService;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -127,17 +135,47 @@ public class ApiController {
|
|||||||
@GetMapping(value = "/api/transcriptions/{transcriptionId}/file/{fileId}/result",
|
@GetMapping(value = "/api/transcriptions/{transcriptionId}/file/{fileId}/result",
|
||||||
consumes = MediaType.ALL_VALUE,
|
consumes = MediaType.ALL_VALUE,
|
||||||
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
public InputStream getTranscribedResult(
|
public ResponseEntity<InputStreamResource> getTranscribedResult(
|
||||||
Principal owner,
|
Principal owner,
|
||||||
@PathVariable("transcriptionId") String transcriptionId,
|
@PathVariable("transcriptionId") String transcriptionId,
|
||||||
@PathVariable("fileId") String fileId)
|
@PathVariable("fileId") String fileId)
|
||||||
{
|
{
|
||||||
// access control
|
// access control
|
||||||
transcriptionService.getTranscription(owner, UUID.fromString(transcriptionId))
|
Transcription transcription = transcriptionService.getTranscription(owner, UUID.fromString(transcriptionId))
|
||||||
.orElseThrow(() -> new TranscriptionNotFound(transcriptionId));
|
.orElseThrow(() -> new TranscriptionNotFound(transcriptionId));
|
||||||
|
|
||||||
// todo
|
try {
|
||||||
throw new UnsupportedOperationException("NYI");
|
TranscribedResult transcribedResult = transcriptionService.getTranscribedResult(
|
||||||
|
transcription,
|
||||||
|
UUID.fromString(fileId));
|
||||||
|
String extension = getExtensionForOutputFormat(transcription.outputFormat());
|
||||||
|
String filename = transcribedResult.sourceFile().filename() + "." + extension;
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
|
||||||
|
.header("Content-Length", Long.toString(transcribedResult.size()))
|
||||||
|
.body(new InputStreamResource(transcribedResult.data()));
|
||||||
|
} catch (NotSubmittedForTranscription e) {
|
||||||
|
throw new NoTranscribedResultAvailable("The file has not been submitted for transcription");
|
||||||
|
} catch (TranscriptionJobStillPending e) {
|
||||||
|
throw new NoTranscribedResultAvailable("The file is still processing, try again later");
|
||||||
|
} catch (TranscriptionJobFailed e) {
|
||||||
|
throw new NoTranscribedResultAvailable("The transcription failed: " + e.errorMessage());
|
||||||
|
} catch (NoSuchSourceFile e) {
|
||||||
|
throw new SourceFileNotFound(e.transcription(), e.fileId());
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(System.Logger.Level.ERROR, "Failed to get transcribed result", e);
|
||||||
|
throw new ErrorResponseException(HttpStatus.INTERNAL_SERVER_ERROR, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getExtensionForOutputFormat(OutputFormat outputFormat) {
|
||||||
|
return switch (outputFormat) {
|
||||||
|
case PLAIN_TEXT -> "txt";
|
||||||
|
case VTT -> "vtt";
|
||||||
|
case SRT -> "srt";
|
||||||
|
case TSV -> "tsv";
|
||||||
|
case JSON -> "json";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/api/transcriptions/{id}/job", consumes = "*/*")
|
@PostMapping(value = "/api/transcriptions/{id}/job", consumes = "*/*")
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package se.su.dsv.whisperapi.api;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.ErrorResponseException;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
public class NoTranscribedResultAvailable extends ErrorResponseException {
|
||||||
|
public NoTranscribedResultAvailable(String detail) {
|
||||||
|
super(HttpStatus.CONFLICT);
|
||||||
|
setType(URI.create("https://gitea.dsv.su.se/DMC/whisper-frontend/wiki/Errors#no-transcribed-result-available"));
|
||||||
|
setTitle("No transcribed result available");
|
||||||
|
setDetail(detail);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package se.su.dsv.whisperapi.api;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.ErrorResponseException;
|
||||||
|
import se.su.dsv.whisperapi.core.Transcription;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SourceFileNotFound extends ErrorResponseException {
|
||||||
|
public SourceFileNotFound(Transcription transcription, UUID uuid) {
|
||||||
|
super(HttpStatus.BAD_REQUEST);
|
||||||
|
setType(URI.create("https://gitea.dsv.su.se/DMC/whisper-frontend/wiki/Errors#transcription-file-not-found"));
|
||||||
|
setTitle("Transcription source file not found");
|
||||||
|
setDetail("No file with id '" + uuid + "' exists in transcription with id '" + transcription.id() + "'");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package se.su.dsv.whisperapi.core;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class NoSuchSourceFile extends Exception {
|
||||||
|
private final Transcription transcription;
|
||||||
|
private final UUID fileId;
|
||||||
|
|
||||||
|
public NoSuchSourceFile(Transcription transcription, UUID fileId) {
|
||||||
|
this.transcription = transcription;
|
||||||
|
this.fileId = fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transcription transcription() {
|
||||||
|
return transcription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID fileId() {
|
||||||
|
return fileId;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package se.su.dsv.whisperapi.core;
|
||||||
|
|
||||||
|
public class NotSubmittedForTranscription extends Exception {
|
||||||
|
public NotSubmittedForTranscription() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package se.su.dsv.whisperapi.core;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public record TranscribedResult(SourceFile sourceFile, long size, InputStream data) {
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package se.su.dsv.whisperapi.core;
|
||||||
|
|
||||||
|
public class TranscriptionJobFailed extends Exception {
|
||||||
|
private final String errorMessage;
|
||||||
|
|
||||||
|
public TranscriptionJobFailed(String errorMessage) {
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String errorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package se.su.dsv.whisperapi.core;
|
||||||
|
|
||||||
|
public class TranscriptionJobStillPending extends Exception {
|
||||||
|
public TranscriptionJobStillPending() {
|
||||||
|
}
|
||||||
|
}
|
@ -37,4 +37,8 @@ public interface TranscriptionRepository {
|
|||||||
void markAsCompleted(Transcription transcription);
|
void markAsCompleted(Transcription transcription);
|
||||||
|
|
||||||
void increaseFailureCount(Transcription transcription, Instant now);
|
void increaseFailureCount(Transcription transcription, Instant now);
|
||||||
|
|
||||||
|
Optional<SourceFile> getFile(Transcription transcription, UUID fileId);
|
||||||
|
|
||||||
|
Optional<Job> findJobBySourceFile(SourceFile sourceFile);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
@ -182,6 +181,7 @@ public class TranscriptionService {
|
|||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
return response.statusCode() == 200;
|
return response.statusCode() == 200;
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
LOGGER.log(System.Logger.Level.ERROR, "Failed to notify owner", e);
|
LOGGER.log(System.Logger.Level.ERROR, "Failed to notify owner", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -194,4 +194,27 @@ public class TranscriptionService {
|
|||||||
private void markTranscriptionAsCompleted(Transcription transcription) {
|
private void markTranscriptionAsCompleted(Transcription transcription) {
|
||||||
transcriptionRepository.markAsCompleted(transcription);
|
transcriptionRepository.markAsCompleted(transcription);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TranscribedResult getTranscribedResult(Transcription transcription, UUID fileId)
|
||||||
|
throws
|
||||||
|
NotSubmittedForTranscription,
|
||||||
|
TranscriptionJobStillPending,
|
||||||
|
TranscriptionJobFailed,
|
||||||
|
IOException,
|
||||||
|
NoSuchSourceFile
|
||||||
|
{
|
||||||
|
SourceFile sourceFile = transcriptionRepository.getFile(transcription, fileId)
|
||||||
|
.orElseThrow(() -> new NoSuchSourceFile(transcription, fileId));
|
||||||
|
Job job = transcriptionRepository.findJobBySourceFile(sourceFile)
|
||||||
|
.orElseThrow(() -> new NotSubmittedForTranscription());
|
||||||
|
|
||||||
|
return switch (job.status()) {
|
||||||
|
case Job.Status.Completed(JobCompletion.Success(Path resultFile)) ->
|
||||||
|
new TranscribedResult(sourceFile, Files.size(resultFile), Files.newInputStream(resultFile));
|
||||||
|
case Job.Status.Completed(JobCompletion.Failure(String errorMessage)) ->
|
||||||
|
throw new TranscriptionJobFailed(errorMessage);
|
||||||
|
case Job.Status.Pending ignored ->
|
||||||
|
throw new TranscriptionJobStillPending();
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user