WIP: Submit transcoding jobs via a HTTP API #6

Draft
ansv7779 wants to merge 22 commits from api-submission into master
10 changed files with 138 additions and 16 deletions
Showing only changes of commit 515b2aa642 - Show all commits

View File

@ -6,6 +6,7 @@ import se.su.dsv.whisperapi.core.Job;
import se.su.dsv.whisperapi.core.JobCompletion;
import se.su.dsv.whisperapi.core.NotificationStatus;
import se.su.dsv.whisperapi.core.OutputFormat;
import se.su.dsv.whisperapi.core.SourceFile;
import se.su.dsv.whisperapi.core.TranscriptionRepository;
import se.su.dsv.whisperapi.core.Transcription;
@ -55,14 +56,16 @@ public class JDBCTranscriptionRepository implements TranscriptionRepository {
}
@Override
public void addFileToTranscription(Transcription transcription, String filename) {
public void addFileToTranscription(Transcription transcription, SourceFile sourceFile) {
jdbcClient.sql("""
INSERT INTO transcriptions_files (transcription_id, filename)
VALUES (:transcription_id, :filename)
INSERT INTO transcriptions_files (id, transcription_id, filename, transcribed_result_uri)
VALUES (:id, :transcription_id, :filename, :result_file_download_uri)
ON DUPLICATE KEY UPDATE filename = :filename
""")
.param("transcription_id", transcription.id())
.param("filename", filename)
.param("id", sourceFile.id())
.param("filename", sourceFile.filename())
.param("result_file_download_uri", sourceFile.downloadUri().toString())
.update();
}

View File

@ -1,6 +1,8 @@
package se.su.dsv.whisperapi.api;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
@ -15,7 +17,7 @@ import se.su.dsv.whisperapi.core.JobCompletion;
import se.su.dsv.whisperapi.core.JobNotFound;
import se.su.dsv.whisperapi.core.OutputFormat;
import se.su.dsv.whisperapi.core.Transcription;
import se.su.dsv.whisperapi.core.TranscriptionFile;
import se.su.dsv.whisperapi.core.SourceFileUpload;
import se.su.dsv.whisperapi.core.TranscriptionService;
import java.io.IOException;
@ -78,6 +80,7 @@ public class ApiController {
Principal owner,
@PathVariable("id") String id,
@RequestHeader("X-Filename") String filename,
UriComponentsBuilder uriComponentsBuilder,
InputStream fileData)
{
try {
@ -87,7 +90,22 @@ public class ApiController {
UUID uuid = UUID.fromString(id);
Transcription transcription = transcriptionService.getTranscription(owner, uuid)
.orElseThrow(() -> new TranscriptionNotFound(id));
transcriptionService.addFileToBeTranscribed(transcription, new TranscriptionFile(filename, fileData));
transcriptionService.addFileToBeTranscribed(
transcription,
new SourceFileUpload(filename, fileData),
// The URI is generated here at upload since we need to be in a web context to generate it.
// It is likely that the source who uploaded is the same as who will download the result
// so using this as the web context seem like the best spot, the other option would be at
// submission time.
transcribedFileId -> uriComponentsBuilder.cloneBuilder()
.pathSegment("api")
.pathSegment("transcriptions")
.pathSegment(id)
.pathSegment("file")
.pathSegment(transcribedFileId.toString())
.pathSegment("result")
.build()
.toUri());
return ResponseEntity.accepted().build();
} catch (IllegalArgumentException ignored) {
// Invalid UUID
@ -98,6 +116,30 @@ public class ApiController {
}
}
/**
* Get the transcribed result for a given source file if it was transcribed successfully.
*
* @param owner owner of the transcription job
* @param transcriptionId id of the transcription job
* @param fileId id of the source file
* @return the transcribed result
*/
@GetMapping(value = "/api/transcriptions/{transcriptionId}/file/{fileId}/result",
consumes = MediaType.ALL_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public InputStream getTranscribedResult(
Principal owner,
@PathVariable("transcriptionId") String transcriptionId,
@PathVariable("fileId") String fileId)
{
// access control
transcriptionService.getTranscription(owner, UUID.fromString(transcriptionId))
.orElseThrow(() -> new TranscriptionNotFound(transcriptionId));
// todo
throw new UnsupportedOperationException("NYI");
}
@PostMapping(value = "/api/transcriptions/{id}/job", consumes = "*/*")
public ResponseEntity<Void> submitTranscriptionJob(
Principal owner,

View File

@ -0,0 +1,29 @@
package se.su.dsv.whisperapi.core;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.util.List;
import java.util.UUID;
public record Callback(
@JsonProperty("transcription_id") UUID transcriptionId,
@JsonProperty("files") List<File> files)
{
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "result")
@JsonSubTypes({
@JsonSubTypes.Type(value = File.Transcribed.class, name = "success"),
@JsonSubTypes.Type(value = File.Failed.class, name = "failure")
})
sealed interface File {
record Transcribed(
@JsonProperty("original_file_name") String originalFilename,
@JsonProperty("transcription_download_link") String transcriptionDownloadLink)
implements File {}
record Failed(
@JsonProperty("original_file_name") String originalFilename,
@JsonProperty("error_message") String errorMessage)
implements File {}
}
}

View File

@ -0,0 +1,11 @@
package se.su.dsv.whisperapi.core;
import java.net.URI;
import java.util.UUID;
/**
* @param filename the user provided file name
* @param downloadUri a pre-generated download link for any potential future transcription result
*/
public record SourceFile(UUID id, String filename, URI downloadUri) {
}

View File

@ -0,0 +1,6 @@
package se.su.dsv.whisperapi.core;
import java.io.InputStream;
public record SourceFileUpload(String filename, InputStream data) {
}

View File

@ -0,0 +1,9 @@
package se.su.dsv.whisperapi.core;
import java.net.URI;
import java.util.UUID;
@FunctionalInterface
public interface TranscribedFileDownloadUriGenerator {
URI generateDownloadUri(UUID transcribedFileId);
}

View File

@ -1,6 +0,0 @@
package se.su.dsv.whisperapi.core;
import java.io.InputStream;
public record TranscriptionFile(String filename, InputStream data) {
}

View File

@ -11,7 +11,7 @@ public interface TranscriptionRepository {
Optional<Transcription> findByOwnerAndId(Principal owner, UUID uuid);
void addFileToTranscription(Transcription transcription, String filename);
void addFileToTranscription(Transcription transcription, SourceFile sourceFile);
/**
* @return the list of filenames that have been {@link #addFileToTranscription added}

View File

@ -50,16 +50,22 @@ public class TranscriptionService {
return transcriptionRepository.findByOwnerAndId(owner, uuid);
}
public void addFileToBeTranscribed(Transcription transcription, TranscriptionFile file)
public void addFileToBeTranscribed(
Transcription transcription,
SourceFileUpload file,
TranscribedFileDownloadUriGenerator downloadUriGenerator)
throws IOException
{
Path fileToBeTranscribed = fileDirectory.resolve(transcription.id().toString()).resolve(file.filename());
UUID uuid = UUID.randomUUID();
URI downloadUri = downloadUriGenerator.generateDownloadUri(uuid);
Path fileToBeTranscribed = fileDirectory.resolve(transcription.id().toString()).resolve(uuid.toString());
Files.createDirectories(fileToBeTranscribed.getParent());
try (var out = Files.newOutputStream(fileToBeTranscribed, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
var in = file.data())
{
in.transferTo(out);
transcriptionRepository.addFileToTranscription(transcription, file.filename());
SourceFile sourceFile = new SourceFile(uuid, file.filename(), downloadUri);
transcriptionRepository.addFileToTranscription(transcription, sourceFile);
}
}
@ -150,6 +156,21 @@ public class TranscriptionService {
private boolean notifyOwner(final Transcription transcription, List<Job> jobs) {
URI callbackUri = transcription.callbackUri();
List<Callback.File> files = jobs.stream()
.<Callback.File>map(job -> {
switch (job.status()) {
case Job.Status.Completed(JobCompletion.Success ignored) -> {
return new Callback.File.Transcribed("<unknown>", "http://"); //job.transcribedDownloadLink());
}
case Job.Status.Completed(JobCompletion.Failure(String errorMessage)) -> {
return new Callback.File.Failed("<unknown>", errorMessage);
}
case Job.Status.Pending() -> throw new IllegalStateException("Job should be completed");
}
})
.toList();
Callback callback = new Callback(transcription.id(), files);
ObjectMapper objectMapper = new ObjectMapper();
try (HttpClient client = HttpClient.newHttpClient()) {
HttpRequest request = HttpRequest.newBuilder()
.uri(callbackUri)

View File

@ -0,0 +1,7 @@
ALTER TABLE transcriptions_files
ADD COLUMN id UUID NOT NULL FIRST,
ADD COLUMN transcribed_result_uri VARCHAR(255) NOT NULL AFTER filename;
ALTER TABLE transcriptions_files ADD INDEX FK_transcriptions_files_transcription (transcription_id);
ALTER TABLE transcriptions_files DROP PRIMARY KEY;
ALTER TABLE transcriptions_files ADD PRIMARY KEY (id);