WIP: Submit transcoding jobs via a HTTP API #6
@ -210,6 +210,33 @@ public class JDBCTranscriptionRepository implements TranscriptionRepository {
|
||||
.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> {
|
||||
INSTANCE;
|
||||
|
||||
|
@ -30,7 +30,15 @@ public class WhisperApiApplication {
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/api/transcriptions/job/callback/**").permitAll()
|
||||
.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())
|
||||
.build();
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
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.ResponseEntity;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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.JobCompletion;
|
||||
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.TranscribedResult;
|
||||
import se.su.dsv.whisperapi.core.Transcription;
|
||||
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 java.io.IOException;
|
||||
@ -127,17 +135,47 @@ public class ApiController {
|
||||
@GetMapping(value = "/api/transcriptions/{transcriptionId}/file/{fileId}/result",
|
||||
consumes = MediaType.ALL_VALUE,
|
||||
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
public InputStream getTranscribedResult(
|
||||
public ResponseEntity<InputStreamResource> getTranscribedResult(
|
||||
Principal owner,
|
||||
@PathVariable("transcriptionId") String transcriptionId,
|
||||
@PathVariable("fileId") String fileId)
|
||||
{
|
||||
// access control
|
||||
transcriptionService.getTranscription(owner, UUID.fromString(transcriptionId))
|
||||
Transcription transcription = transcriptionService.getTranscription(owner, UUID.fromString(transcriptionId))
|
||||
.orElseThrow(() -> new TranscriptionNotFound(transcriptionId));
|
||||
|
||||
// todo
|
||||
throw new UnsupportedOperationException("NYI");
|
||||
try {
|
||||
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 = "*/*")
|
||||
|
@ -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 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 java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
@ -182,6 +181,7 @@ public class TranscriptionService {
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
return response.statusCode() == 200;
|
||||
} catch (IOException | InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOGGER.log(System.Logger.Level.ERROR, "Failed to notify owner", e);
|
||||
return false;
|
||||
}
|
||||
@ -194,4 +194,27 @@ public class TranscriptionService {
|
||||
private void markTranscriptionAsCompleted(Transcription 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