WIP: Submit transcoding jobs via a HTTP API #6

Draft
ansv7779 wants to merge 22 commits from api-submission into master
14 changed files with 222 additions and 0 deletions
Showing only changes of commit 7ee1b2a745 - Show all commits

View File

@ -0,0 +1,26 @@
package se.su.dsv.whisperapi;
import org.springframework.jdbc.core.simple.JdbcClient;
import se.su.dsv.whisperapi.core.TransactionRepository;
import se.su.dsv.whisperapi.core.Transcription;
public class JDBCTransactionRepository implements TransactionRepository {
private final JdbcClient jdbcClient;
public JDBCTransactionRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
@Override
public void save(Transcription transcription) {
jdbcClient.sql("""
INSERT INTO transcriptions (id, owner, callback_uri, output_format)
VALUES (:id, :owner, :callback_uri, :output_format)
""")
.param("id", transcription.id())
.param("owner", transcription.owner().getName())
.param("callback_uri", transcription.callbackUri().toString())
.param("output_format", transcription.outputFormat().name())
.update();
}
}

View File

@ -3,9 +3,13 @@ package se.su.dsv.whisperapi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import se.su.dsv.whisperapi.core.TransactionRepository;
import se.su.dsv.whisperapi.core.TranscriptionService;
@SpringBootApplication
public class WhisperApiApplication {
@ -15,10 +19,32 @@ public class WhisperApiApplication {
}
@Bean
@Order(1)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.build();
}
@Bean
@Order(2)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorize -> authorize.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults());
return http.build();
}
@Bean
public TranscriptionService transcriptionService(TransactionRepository transcriptionRepository) {
return new TranscriptionService(transcriptionRepository);
}
@Bean
public JDBCTransactionRepository jdbcTransactionRepository(JdbcClient jdbcClient) {
return new JDBCTransactionRepository(jdbcClient);
}
}

View File

@ -0,0 +1,57 @@
package se.su.dsv.whisperapi.api;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import se.su.dsv.whisperapi.core.CreateTranscription;
import se.su.dsv.whisperapi.core.OutputFormat;
import se.su.dsv.whisperapi.core.Transcription;
import se.su.dsv.whisperapi.core.TranscriptionService;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
/**
* For submitting jobs programmatically via a JSON HTTP API.
* Submitting jobs programmatically is only available to applications with
* shared filesystem access.
*/
@RestController
@RequestMapping(consumes = "application/json", produces = "application/json")
public class ApiController {
private final TranscriptionService transcriptionService;
public ApiController(TranscriptionService transcriptionService) {
this.transcriptionService = transcriptionService;
}
@PostMapping("/api/transcriptions")
public TranscriptionCreatedResponse submitTranscriptionJob(
Principal owner,
@RequestBody CreateTranscriptionRequest createTranscriptionRequest)
{
try {
URI callbackUri = new URI(createTranscriptionRequest.callback());
OutputFormat outputFormat = parseOutputFormat(createTranscriptionRequest.outputFormat());
CreateTranscription createTranscription = new CreateTranscription(owner, callbackUri, outputFormat);
Transcription transcription = transcriptionService.createTranscription(createTranscription);
return new TranscriptionCreatedResponse(transcription.id());
} catch (URISyntaxException e) {
throw new InvalidCallbackUri(createTranscriptionRequest.callback());
}
}
private OutputFormat parseOutputFormat(String outputFormat) {
return switch (outputFormat) {
case "txt" -> OutputFormat.PLAIN_TEXT;
case "vtt" -> OutputFormat.VTT;
case "srt" -> OutputFormat.SRT;
case "tsv" -> OutputFormat.TSV;
case "json" -> OutputFormat.JSON;
default -> throw new InvalidOutputFormat(outputFormat);
};
}
}

View File

@ -0,0 +1,9 @@
package se.su.dsv.whisperapi.api;
import com.fasterxml.jackson.annotation.JsonProperty;
public record CreateTranscriptionRequest(
@JsonProperty(value = "callback", required = true) String callback,
@JsonProperty(value = "outputformat", required = true) String outputFormat)
{
}

View File

@ -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 InvalidCallbackUri extends ErrorResponseException {
public InvalidCallbackUri(String callbackUri) {
super(HttpStatus.BAD_REQUEST);
setTitle("Invalid callback URI");
setType(URI.create("https://gitea.dsv.su.se/DMC/whisper-frontend/wiki/Errors#invalid-callback-uri"));
setDetail("The callback '" + callbackUri + "' is not a valid URI");
}
}

View File

@ -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 InvalidOutputFormat extends ErrorResponseException {
public InvalidOutputFormat(String outputFormat) {
super(HttpStatus.BAD_REQUEST);
setType(URI.create("https://gitea.dsv.su.se/DMC/whisper-frontend/wiki/Errors#invalid-output-format"));
setTitle("Invalid output format");
setDetail("The output format '" + outputFormat + "' is not supported.");
}
}

View File

@ -0,0 +1,10 @@
package se.su.dsv.whisperapi.api;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.UUID;
public record TranscriptionCreatedResponse(
@JsonProperty(value = "id", required = true) UUID id)
{
}

View File

@ -0,0 +1,7 @@
package se.su.dsv.whisperapi.core;
import java.net.URI;
import java.security.Principal;
public record CreateTranscription(Principal owner, URI callbackUri, OutputFormat outputFormat) {
}

View File

@ -0,0 +1,9 @@
package se.su.dsv.whisperapi.core;
public enum OutputFormat {
PLAIN_TEXT,
VTT,
SRT,
TSV,
JSON
}

View File

@ -0,0 +1,5 @@
package se.su.dsv.whisperapi.core;
public interface TransactionRepository {
void save(Transcription transcription);
}

View File

@ -0,0 +1,8 @@
package se.su.dsv.whisperapi.core;
import java.net.URI;
import java.security.Principal;
import java.util.UUID;
public record Transcription(UUID id, Principal owner, URI callbackUri, OutputFormat outputFormat) {
}

View File

@ -0,0 +1,23 @@
package se.su.dsv.whisperapi.core;
import java.util.UUID;
public class TranscriptionService {
private final TransactionRepository transactionRepository;
public TranscriptionService(TransactionRepository transactionRepository) {
this.transactionRepository = transactionRepository;
}
public Transcription createTranscription(CreateTranscription createTranscription) {
UUID id = UUID.randomUUID();
Transcription transcription = new Transcription(
id,
createTranscription.owner(),
createTranscription.callbackUri(),
createTranscription.outputFormat());
transactionRepository.save(transcription);
return transcription;
}
}

View File

@ -12,3 +12,7 @@ spring.security.oauth2.client.provider.su.authorization-uri=${OAUTH2_AUTH_URI}
spring.security.oauth2.client.provider.su.token-uri=${OAUTH2_TOKEN_URI}
spring.security.oauth2.client.provider.su.user-info-uri=${OAUTH2_USER_INFO_URI}
spring.security.oauth2.client.provider.su.user-name-attribute=sub
spring.mvc.problemdetails.enabled=true
# Disable logging of resolved exceptions or every ErrorResponseException (ProblemDetails)
# that is correctly handled will be logged as a warning
spring.mvc.log-resolved-exception=false

View File

@ -0,0 +1,8 @@
CREATE TABLE transcriptions (
id UUID NOT NULL,
owner VARCHAR(255) NOT NULL,
callback_uri VARCHAR(255),
output_format VARCHAR(32) NOT NULL,
PRIMARY KEY (id),
INDEX I_transcriptions_owner (owner)
);