WIP: Submit transcoding jobs via a HTTP API #6
@ -6,6 +6,7 @@ import se.su.dsv.whisperapi.core.Job;
|
|||||||
import se.su.dsv.whisperapi.core.JobCompletion;
|
import se.su.dsv.whisperapi.core.JobCompletion;
|
||||||
import se.su.dsv.whisperapi.core.NotificationStatus;
|
import se.su.dsv.whisperapi.core.NotificationStatus;
|
||||||
import se.su.dsv.whisperapi.core.OutputFormat;
|
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.TranscriptionRepository;
|
||||||
import se.su.dsv.whisperapi.core.Transcription;
|
import se.su.dsv.whisperapi.core.Transcription;
|
||||||
|
|
||||||
@ -55,14 +56,16 @@ public class JDBCTranscriptionRepository implements TranscriptionRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addFileToTranscription(Transcription transcription, String filename) {
|
public void addFileToTranscription(Transcription transcription, SourceFile sourceFile) {
|
||||||
jdbcClient.sql("""
|
jdbcClient.sql("""
|
||||||
INSERT INTO transcriptions_files (transcription_id, filename)
|
INSERT INTO transcriptions_files (id, transcription_id, filename, transcribed_result_uri)
|
||||||
VALUES (:transcription_id, :filename)
|
VALUES (:id, :transcription_id, :filename, :result_file_download_uri)
|
||||||
ON DUPLICATE KEY UPDATE filename = :filename
|
ON DUPLICATE KEY UPDATE filename = :filename
|
||||||
""")
|
""")
|
||||||
.param("transcription_id", transcription.id())
|
.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();
|
.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package se.su.dsv.whisperapi.api;
|
package se.su.dsv.whisperapi.api;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
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.JobNotFound;
|
||||||
import se.su.dsv.whisperapi.core.OutputFormat;
|
import se.su.dsv.whisperapi.core.OutputFormat;
|
||||||
import se.su.dsv.whisperapi.core.Transcription;
|
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 se.su.dsv.whisperapi.core.TranscriptionService;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -78,6 +80,7 @@ public class ApiController {
|
|||||||
Principal owner,
|
Principal owner,
|
||||||
@PathVariable("id") String id,
|
@PathVariable("id") String id,
|
||||||
@RequestHeader("X-Filename") String filename,
|
@RequestHeader("X-Filename") String filename,
|
||||||
|
UriComponentsBuilder uriComponentsBuilder,
|
||||||
InputStream fileData)
|
InputStream fileData)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@ -87,7 +90,22 @@ public class ApiController {
|
|||||||
UUID uuid = UUID.fromString(id);
|
UUID uuid = UUID.fromString(id);
|
||||||
Transcription transcription = transcriptionService.getTranscription(owner, uuid)
|
Transcription transcription = transcriptionService.getTranscription(owner, uuid)
|
||||||
.orElseThrow(() -> new TranscriptionNotFound(id));
|
.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();
|
return ResponseEntity.accepted().build();
|
||||||
} catch (IllegalArgumentException ignored) {
|
} catch (IllegalArgumentException ignored) {
|
||||||
// Invalid UUID
|
// 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 = "*/*")
|
@PostMapping(value = "/api/transcriptions/{id}/job", consumes = "*/*")
|
||||||
public ResponseEntity<Void> submitTranscriptionJob(
|
public ResponseEntity<Void> submitTranscriptionJob(
|
||||||
Principal owner,
|
Principal owner,
|
||||||
|
29
src/main/java/se/su/dsv/whisperapi/core/Callback.java
Normal file
29
src/main/java/se/su/dsv/whisperapi/core/Callback.java
Normal 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 {}
|
||||||
|
}
|
||||||
|
}
|
11
src/main/java/se/su/dsv/whisperapi/core/SourceFile.java
Normal file
11
src/main/java/se/su/dsv/whisperapi/core/SourceFile.java
Normal 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) {
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package se.su.dsv.whisperapi.core;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public record SourceFileUpload(String filename, InputStream data) {
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
package se.su.dsv.whisperapi.core;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public record TranscriptionFile(String filename, InputStream data) {
|
|
||||||
}
|
|
@ -11,7 +11,7 @@ public interface TranscriptionRepository {
|
|||||||
|
|
||||||
Optional<Transcription> findByOwnerAndId(Principal owner, UUID uuid);
|
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}
|
* @return the list of filenames that have been {@link #addFileToTranscription added}
|
||||||
|
@ -50,16 +50,22 @@ public class TranscriptionService {
|
|||||||
return transcriptionRepository.findByOwnerAndId(owner, uuid);
|
return transcriptionRepository.findByOwnerAndId(owner, uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addFileToBeTranscribed(Transcription transcription, TranscriptionFile file)
|
public void addFileToBeTranscribed(
|
||||||
|
Transcription transcription,
|
||||||
|
SourceFileUpload file,
|
||||||
|
TranscribedFileDownloadUriGenerator downloadUriGenerator)
|
||||||
throws IOException
|
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());
|
Files.createDirectories(fileToBeTranscribed.getParent());
|
||||||
try (var out = Files.newOutputStream(fileToBeTranscribed, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
try (var out = Files.newOutputStream(fileToBeTranscribed, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||||
var in = file.data())
|
var in = file.data())
|
||||||
{
|
{
|
||||||
in.transferTo(out);
|
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) {
|
private boolean notifyOwner(final Transcription transcription, List<Job> jobs) {
|
||||||
URI callbackUri = transcription.callbackUri();
|
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()) {
|
try (HttpClient client = HttpClient.newHttpClient()) {
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(callbackUri)
|
.uri(callbackUri)
|
||||||
|
@ -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);
|
Loading…
x
Reference in New Issue
Block a user