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;
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.LoggerFactory;
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.RequestParam;
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.FileMetadata;
import se.su.dsv.seshat.entities.JobStatus;
import se.su.dsv.seshat.services.JobProcessorService;
import se.su.dsv.seshat.services.StorageService;
import se.su.dsv.seshat.services.UserService;
import java.io.File;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Controller
@ -44,42 +46,53 @@ public class FileController {
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")
public String showFileManagementPage(Authentication authentication, Model model) {
AppUser user = userService.getUserByUsername(authentication.getName());
List<FileMetadata> files = storageService.getUserTranscriptons(user);
List<FileMetadata> uploaded = storageService.getUserUplaods(user);
List<FileMetadata> statuses = uploaded.stream()
List<FileMetadata> fileUploadStatuses = uploaded.stream()
.filter(file -> file.getJobStatus() != null)
.toList();
// Map<String, List<FileMetadata>> filesByDirectory = files.stream().collect(Collectors.groupingBy(FileMetadata::getOutputDirectory));
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
));
boolean hasJobInProgress = hasJobInProgress(fileUploadStatuses);
Map<FileMetadata, List<FileMetadata>> sortedFilesByDirectory = storageService.getTranscribedFilesByDirectory(user);
model.addAttribute("filesByDirectory", sortedFilesByDirectory);
model.addAttribute("statuses", statuses);
model.addAttribute("fileUploadStatuses", fileUploadStatuses);
model.addAttribute("hasJobInProgress", hasJobInProgress);
return "file-management";
}
@ -100,6 +113,7 @@ public class FileController {
model.addAttribute("message", "File uploaded successfully. Transcription will start shortly.");
} catch (Exception e) {
logger.error("Error uploading file", e);
model.addAttribute("error", "File upload failed: " + e.getMessage());
}
@ -169,4 +183,16 @@ public class FileController {
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.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
@ -176,7 +179,7 @@ public class StorageService {
} catch (IllegalArgumentException e) {
return false;
} catch (IOException e) {
logger.info("Failed to delete file or directory: " + e.getMessage());
logger.info("Failed to delete file or directory: {}", e.getMessage());
}
} else {
return false;
@ -227,15 +230,38 @@ public class StorageService {
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) {
return filename.replaceAll("[^a-zA-Z0-9.-]", "_");
}
private String fileNameAndUploadedTime(FileMetadata file) {
String fileName = file.getFileName();
LocalDateTime uploadedAt = file.getUploadedAt();
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) {

View File

@ -11,11 +11,11 @@ import java.io.IOException;
public class Transcriber {
private static final Logger logger = LoggerFactory.getLogger(Transcriber.class);
private String modelPath;
private String selectedModel;
private String workingDirectory;
private final String modelPath;
private final String selectedModel;
private final String workingDirectory;
ProcessBuilder startVirtualEnv;
private String selectedDevice;
private final String selectedDevice;
public Transcriber(WhisperProperties whisperProperties) {
modelPath = whisperProperties.modelPath();
@ -32,7 +32,7 @@ public class Transcriber {
Process p = startVirtualEnv.start();
p.waitFor();
} 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>
<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>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -28,7 +28,6 @@
<main class="container mt-4">
<h2>File Management</h2>
<!-- File Upload Section -->
<section>
<h3>Upload File</h3>
@ -53,10 +52,9 @@
<button type="submit" class="btn btn-primary">Upload</button>
</form>
</section>
<!-- File Status Section -->
<section th:if="${statuses != null && !statuses.isEmpty()}" class="mt-5">
<h3>File Upload Statuses</h3>
<section th:if="${fileUploadStatuses != null && !fileUploadStatuses.isEmpty()}" class="mt-5">
<h3>File Upload History</h3>
<div class="table-wrapper">
<table class="table">
<thead>
@ -67,15 +65,34 @@
<th>Status</th>
</tr>
</thead>
<tbody>
<tr th:each="status : ${statuses}">
<td th:text="${status.fileName}">File Name</td>
<td th:text="${#temporals.format(status.getUploadedAt(), 'yyyy-MM-dd')}">Upload Date</td>
<td th:text="${#temporals.format(status.getUploadedAt(), 'HH:mm:ss')}">Upload time</td>
<td>
<span class="badge bg-primary" th:text="${status.jobStatus}">Status</span>
</td>
</tr>
<tbody th:if="${hasJobInProgress == true}"
th:fragment="file-upload-statuses"
hx-get="/files/file-upload-statuses"
hx-trigger="load delay:10s"
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>
<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>
</tr>
</tbody>
</table>
</div>
@ -86,7 +103,13 @@
<!-- File Browsing Section -->
<section>
<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}">
<a data-bs-toggle="collapse"
@ -132,8 +155,8 @@
<footer class="bg-primary text-white text-center py-3">
<p>&copy; 2024 Seshat App</p>
</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/htmx/2.0.4/dist/htmx.min.js}"></script>
<script type="text/javascript" th:src="@{/js/script.js}"></script>
</body>
</html>