Preparing for production #8

Merged
niat8586 merged 12 commits from develop into main 2025-02-03 11:29:15 +01:00
4 changed files with 128 additions and 53 deletions
Showing only changes of commit d390552dc3 - Show all commits

View File

@ -1,5 +1,7 @@
package se.su.dsv.seshat.controllers; package se.su.dsv.seshat.controllers;
import io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponse;
import io.github.wimdeblauwe.htmx.spring.boot.mvc.HxRequest;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
@ -15,19 +17,19 @@ 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.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.FragmentsRendering;
import se.su.dsv.seshat.entities.AppUser; import se.su.dsv.seshat.entities.AppUser;
import se.su.dsv.seshat.entities.FileMetadata; import se.su.dsv.seshat.entities.FileMetadata;
import se.su.dsv.seshat.entities.JobStatus;
import se.su.dsv.seshat.services.JobProcessorService; import se.su.dsv.seshat.services.JobProcessorService;
import se.su.dsv.seshat.services.StorageService; import se.su.dsv.seshat.services.StorageService;
import se.su.dsv.seshat.services.UserService; import se.su.dsv.seshat.services.UserService;
import java.io.File; import java.io.File;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Controller @Controller
@ -44,42 +46,53 @@ public class FileController {
this.jobProcessorService = jobProcessorService; this.jobProcessorService = jobProcessorService;
} }
@HxRequest
@GetMapping("/files/transcribed-files")
public String getTranscribedFiles(Authentication authentication, Model model) {
AppUser user = userService.getUserByUsername(authentication.getName());
Map<FileMetadata, List<FileMetadata>> filesByDirectory = storageService.getTranscribedFilesByDirectory(user);
model.addAttribute("filesByDirectory", filesByDirectory);
return "file-management :: transcribed-files";
}
@HxRequest
@GetMapping("/files/file-upload-statuses")
public View getFileUploadStatuses(Authentication authentication, Model model, HtmxResponse htmxResponse) {
AppUser user = userService.getUserByUsername(authentication.getName());
List<FileMetadata> uploaded = storageService.getUserUplaods(user);
List<FileMetadata> fileUploadStatuses = getFileUploadStatuses(uploaded);
boolean hasJobInProgress = hasJobInProgress(fileUploadStatuses);
model.addAttribute("fileUploadStatuses", fileUploadStatuses);
model.addAttribute("hasJobInProgress", hasJobInProgress);
if(hasJobInProgress) {
// return "file-management :: file-upload-statuses";
return FragmentsRendering.with("file-management :: file-upload-statuses").build();
}
htmxResponse.addTrigger("transcription-finished");
return FragmentsRendering.with("file-management :: file-upload-statuses-no-jobs").build();
}
@GetMapping("/files/manage") @GetMapping("/files/manage")
public String showFileManagementPage(Authentication authentication, Model model) { public String showFileManagementPage(Authentication authentication, Model model) {
AppUser user = userService.getUserByUsername(authentication.getName()); AppUser user = userService.getUserByUsername(authentication.getName());
List<FileMetadata> files = storageService.getUserTranscriptons(user);
List<FileMetadata> uploaded = storageService.getUserUplaods(user); List<FileMetadata> uploaded = storageService.getUserUplaods(user);
List<FileMetadata> statuses = uploaded.stream() List<FileMetadata> fileUploadStatuses = uploaded.stream()
.filter(file -> file.getJobStatus() != null) .filter(file -> file.getJobStatus() != null)
.toList(); .toList();
// Map<String, List<FileMetadata>> filesByDirectory = files.stream().collect(Collectors.groupingBy(FileMetadata::getOutputDirectory)); boolean hasJobInProgress = hasJobInProgress(fileUploadStatuses);
Map<FileMetadata, List<FileMetadata>> filesByDirectory = files.stream()
.collect(Collectors.groupingBy(outputFile -> {
return uploaded.stream()
.filter(uploadedFile -> uploadedFile.getOutputDirectory().equals(outputFile.getOutputDirectory()))
.filter(uploadedFile -> outputFile.getSourceFile().equals(uploadedFile.getFilePath()))
.findFirst()
.orElseThrow(/* Will never happen */);
}));
Map<FileMetadata, List<FileMetadata>> sortedFilesByDirectory = filesByDirectory.entrySet().stream()
.sorted(Map.Entry.comparingByKey(Comparator.comparing(FileMetadata::getUploadedAt).reversed()))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> {
e1.addAll(e2);
return e1;
},
LinkedHashMap::new
));
Map<FileMetadata, List<FileMetadata>> sortedFilesByDirectory = storageService.getTranscribedFilesByDirectory(user);
model.addAttribute("filesByDirectory", sortedFilesByDirectory); model.addAttribute("filesByDirectory", sortedFilesByDirectory);
model.addAttribute("statuses", statuses); model.addAttribute("fileUploadStatuses", fileUploadStatuses);
model.addAttribute("hasJobInProgress", hasJobInProgress);
return "file-management"; return "file-management";
} }
@ -100,6 +113,7 @@ public class FileController {
model.addAttribute("message", "File uploaded successfully. Transcription will start shortly."); model.addAttribute("message", "File uploaded successfully. Transcription will start shortly.");
} catch (Exception e) { } catch (Exception e) {
logger.error("Error uploading file", e);
model.addAttribute("error", "File upload failed: " + e.getMessage()); model.addAttribute("error", "File upload failed: " + e.getMessage());
} }
@ -169,4 +183,16 @@ public class FileController {
return "redirect:/files/manage"; return "redirect:/files/manage";
} }
private static List<FileMetadata> getFileUploadStatuses(List<FileMetadata> uploaded) {
return uploaded.stream()
.filter(file -> file.getJobStatus() != null)
.toList();
}
private static boolean hasJobInProgress(List<FileMetadata> fileUploadStatuses) {
return fileUploadStatuses.stream()
.anyMatch(file -> file.getJobStatus().equals(JobStatus.PROCESSING) ||
file.getJobStatus().equals(JobStatus.PENDING));
}
} }

View File

@ -22,7 +22,10 @@ import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
@ -176,7 +179,7 @@ public class StorageService {
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return false; return false;
} catch (IOException e) { } catch (IOException e) {
logger.info("Failed to delete file or directory: " + e.getMessage()); logger.info("Failed to delete file or directory: {}", e.getMessage());
} }
} else { } else {
return false; return false;
@ -227,15 +230,38 @@ public class StorageService {
return zipFile; return zipFile;
} }
public Map<FileMetadata, List<FileMetadata>> getTranscribedFilesByDirectory(AppUser user) {
List<FileMetadata> files = this.getUserTranscriptons(user);
List<FileMetadata> uploaded = this.getUserUplaods(user);
Map<FileMetadata, List<FileMetadata>> filesByDirectory = files.stream()
.collect(Collectors.groupingBy(outputFile -> uploaded.stream()
.filter(uploadedFile -> uploadedFile.getOutputDirectory().equals(outputFile.getOutputDirectory()))
.filter(uploadedFile -> outputFile.getSourceFile().equals(uploadedFile.getFilePath()))
.findFirst()
.orElseThrow(/* Will never happen */)));
return filesByDirectory.entrySet().stream()
.sorted(Map.Entry.comparingByKey(Comparator.comparing(FileMetadata::getUploadedAt).reversed()))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> {
e1.addAll(e2);
return e1;
},
LinkedHashMap::new
));
}
private String sanitizeFilename(String filename) { private String sanitizeFilename(String filename) {
return filename.replaceAll("[^a-zA-Z0-9.-]", "_"); return filename.replaceAll("[^a-zA-Z0-9.-]", "_");
} }
private String fileNameAndUploadedTime(FileMetadata file) { private String fileNameAndUploadedTime(FileMetadata file) {
String fileName = file.getFileName();
LocalDateTime uploadedAt = file.getUploadedAt(); LocalDateTime uploadedAt = file.getUploadedAt();
String uploadedToSeconds = uploadedAt.truncatedTo(ChronoUnit.SECONDS).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); String uploadedToSeconds = uploadedAt.truncatedTo(ChronoUnit.SECONDS).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
return fileNameWithoutExtension(file.getFileName()) + "_" + uploadedToSeconds.replaceAll(":|-", ""); return fileNameWithoutExtension(file.getFileName()) + "_" + uploadedToSeconds.replaceAll("[:\\-]", "");
} }
private String fileNameWithoutExtension(String fileName) { private String fileNameWithoutExtension(String fileName) {

View File

@ -11,11 +11,11 @@ import java.io.IOException;
public class Transcriber { public class Transcriber {
private static final Logger logger = LoggerFactory.getLogger(Transcriber.class); private static final Logger logger = LoggerFactory.getLogger(Transcriber.class);
private String modelPath; private final String modelPath;
private String selectedModel; private final String selectedModel;
private String workingDirectory; private final String workingDirectory;
ProcessBuilder startVirtualEnv; ProcessBuilder startVirtualEnv;
private String selectedDevice; private final String selectedDevice;
public Transcriber(WhisperProperties whisperProperties) { public Transcriber(WhisperProperties whisperProperties) {
modelPath = whisperProperties.modelPath(); modelPath = whisperProperties.modelPath();
@ -32,7 +32,7 @@ public class Transcriber {
Process p = startVirtualEnv.start(); Process p = startVirtualEnv.start();
p.waitFor(); p.waitFor();
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed to activate virtual environment: " + e.getMessage()); logger.error("Failed to activate virtual environment: {}", e.getMessage());
} }
} }

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:hx-get="http://www.w3.org/1999/xhtml" lang="en"> <html xmlns:th="http://www.thymeleaf.org" lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -28,7 +28,6 @@
<main class="container mt-4"> <main class="container mt-4">
<h2>File Management</h2> <h2>File Management</h2>
<!-- File Upload Section --> <!-- File Upload Section -->
<section> <section>
<h3>Upload File</h3> <h3>Upload File</h3>
@ -53,10 +52,9 @@
<button type="submit" class="btn btn-primary">Upload</button> <button type="submit" class="btn btn-primary">Upload</button>
</form> </form>
</section> </section>
<!-- File Status Section --> <!-- File Status Section -->
<section th:if="${statuses != null && !statuses.isEmpty()}" class="mt-5"> <section th:if="${fileUploadStatuses != null && !fileUploadStatuses.isEmpty()}" class="mt-5">
<h3>File Upload Statuses</h3> <h3>File Upload History</h3>
<div class="table-wrapper"> <div class="table-wrapper">
<table class="table"> <table class="table">
<thead> <thead>
@ -67,13 +65,32 @@
<th>Status</th> <th>Status</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody th:if="${hasJobInProgress == true}"
<tr th:each="status : ${statuses}"> th:fragment="file-upload-statuses"
<td th:text="${status.fileName}">File Name</td> hx-get="/files/file-upload-statuses"
<td th:text="${#temporals.format(status.getUploadedAt(), 'yyyy-MM-dd')}">Upload Date</td> hx-trigger="load delay:10s"
<td th:text="${#temporals.format(status.getUploadedAt(), 'HH:mm:ss')}">Upload time</td> hx-swap="outerHTML">
<tr th:each="uploadStatus : ${fileUploadStatuses}">
<td th:text="${uploadStatus.fileName}">File Name</td>
<td th:text="${#temporals.format(uploadStatus.getUploadedAt(), 'yyyy-MM-dd')}">Upload Date</td>
<td th:text="${#temporals.format(uploadStatus.getUploadedAt(), 'HH:mm:ss')}">Upload time</td>
<td> <td>
<span class="badge bg-primary" th:text="${status.jobStatus}">Status</span> <span class="badge bg-primary" th:text="${uploadStatus.jobStatus}">Status</span>
<span th:if="${uploadStatus.jobStatus == uploadStatus.jobStatus.PENDING ||
uploadStatus.jobStatus == uploadStatus.jobStatus.PROCESSING}">
<span class="spinner-border spinner-border-sm text-primary" role="status"></span>
</span>
</td>
</tr>
</tbody>
<tbody th:unless="${hasJobInProgress == true}"
th:fragment="file-upload-statuses-no-jobs">
<tr th:each="uploadStatus : ${fileUploadStatuses}">
<td th:text="${uploadStatus.fileName}">File Name</td>
<td th:text="${#temporals.format(uploadStatus.getUploadedAt(), 'yyyy-MM-dd')}">Upload Date</td>
<td th:text="${#temporals.format(uploadStatus.getUploadedAt(), 'HH:mm:ss')}">Upload time</td>
<td>
<span class="badge bg-primary" th:text="${uploadStatus.jobStatus}">Status</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -86,7 +103,13 @@
<!-- File Browsing Section --> <!-- File Browsing Section -->
<section> <section>
<h3>Your Transcribed Files</h3> <h3>Your Transcribed Files</h3>
<form id="bulk-actions-form" method="post"> <form th:fragment="transcribed-files"
id="bulk-actions-form"
hx-trigger="transcription-finished from:body"
hx-get="/files/transcribed-files"
hx-swap="outerHTML"
method="post"
class="transcribed-files">
<div th:each="entry : ${filesByDirectory}"> <div th:each="entry : ${filesByDirectory}">
<a data-bs-toggle="collapse" <a data-bs-toggle="collapse"
@ -132,8 +155,8 @@
<footer class="bg-primary text-white text-center py-3"> <footer class="bg-primary text-white text-center py-3">
<p>&copy; 2024 Seshat App</p> <p>&copy; 2024 Seshat App</p>
</footer> </footer>
<script type="text/javascript" th:src="@{/js/script.js}"></script>
<script type="text/javascript" th:src="@{/3p/bootstrap-5.3.3-dist/js/bootstrap.bundle.min.js}"></script> <script type="text/javascript" th:src="@{/3p/bootstrap-5.3.3-dist/js/bootstrap.bundle.min.js}"></script>
<script type="text/javascript" th:src="@{/3p/htmx/2.0.4/dist/htmx.min.js}"></script> <script type="text/javascript" th:src="@{/3p/htmx/2.0.4/dist/htmx.min.js}"></script>
<script type="text/javascript" th:src="@{/js/script.js}"></script>
</body> </body>
</html> </html>