Preparing for production #8
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,15 +65,34 @@
|
|||||||
<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">
|
||||||
<td>
|
<tr th:each="uploadStatus : ${fileUploadStatuses}">
|
||||||
<span class="badge bg-primary" th:text="${status.jobStatus}">Status</span>
|
<td th:text="${uploadStatus.fileName}">File Name</td>
|
||||||
</td>
|
<td th:text="${#temporals.format(uploadStatus.getUploadedAt(), 'yyyy-MM-dd')}">Upload Date</td>
|
||||||
</tr>
|
<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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -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>© 2024 Seshat App</p>
|
<p>© 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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user