Preparing for production #8
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>© 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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user