improve uiux
This commit is contained in:
@@ -28,7 +28,7 @@ Simple app for uploading MP3/WAV files with real-time WebSocket updates.
|
||||
|
||||
```
|
||||
main.py # FastAPI backend
|
||||
templates/index.html # Frontend template
|
||||
index.html # Frontend template
|
||||
static/favicon.ico # Site icon
|
||||
uploads/ # Where files are stored
|
||||
create_favicon.py # Creates the favicon
|
||||
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
BASE_DIR = Path(__file__).parent
|
||||
UPLOAD_DIR = BASE_DIR / "uploads"
|
||||
STATIC_DIR = BASE_DIR / "static"
|
||||
TEMPLATES_DIR = BASE_DIR / "templates"
|
||||
TEMPLATES_DIR = BASE_DIR #/ "templates"
|
||||
TRANSCRIPT_DIR = BASE_DIR / "transcripts"
|
||||
|
||||
# Configuration
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- Head content remains the same -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Transcriptor Agent</title>
|
||||
<title>Transcriptor</title>
|
||||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
|
||||
<style>
|
||||
/* Existing styles */
|
||||
@@ -169,10 +169,34 @@
|
||||
color: #777;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
/* Process All button styling */
|
||||
.process-all-btn {
|
||||
background-color: #5bc0de;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.download-all-btn {
|
||||
background-color: #337ab7;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.dashboard-actions {
|
||||
display: flex;
|
||||
}
|
||||
.disabled-btn {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Transcriptor Agent</h1>
|
||||
<h1>Transcriptor</h1>
|
||||
|
||||
<div class="upload-form">
|
||||
<form id="uploadForm">
|
||||
@@ -194,7 +218,13 @@
|
||||
|
||||
<!-- File List Dashboard -->
|
||||
<div class="file-dashboard">
|
||||
<h2>Uploaded Files <span id="connectionStatus" class="connection-status connecting">Connecting...</span></h2>
|
||||
<div class="dashboard-header">
|
||||
<h2>Uploaded Files <span id="connectionStatus" class="connection-status connecting">Connecting...</span></h2>
|
||||
<div class="dashboard-actions">
|
||||
<button id="processAllBtn" class="process-all-btn">Process All Pending</button>
|
||||
<button id="downloadAllBtn" class="download-all-btn disabled-btn">Download All Transcripts</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fileList" class="file-list">
|
||||
<p>Loading files...</p>
|
||||
</div>
|
||||
@@ -270,6 +300,12 @@
|
||||
document.getElementById(progressId).textContent = 'Uploaded successfully';
|
||||
document.getElementById(progressId).style.color = '#3c763d';
|
||||
successCount++;
|
||||
|
||||
// Force refresh file list after upload
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
console.log("Requesting file list update after upload");
|
||||
socket.send('getFiles');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
document.getElementById(progressId).textContent = `Error: ${result.detail}`;
|
||||
@@ -335,6 +371,7 @@
|
||||
socket.addEventListener('message', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("WebSocket received data:", data); // Debug what's coming from server
|
||||
updateFileList(data.files);
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
@@ -360,13 +397,19 @@
|
||||
function updateFileList(files) {
|
||||
const fileListElement = document.getElementById('fileList');
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
// Filter out .gitkeep files
|
||||
const filteredFiles = files ? files.filter(file => file.name !== '.gitkeep') : [];
|
||||
|
||||
// Update download button state
|
||||
updateDownloadButtonState(filteredFiles);
|
||||
|
||||
if (!filteredFiles || filteredFiles.length === 0) {
|
||||
fileListElement.innerHTML = '<p>No files uploaded yet.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
files.forEach(file => {
|
||||
filteredFiles.forEach(file => {
|
||||
// Format the file size
|
||||
const fileSizeKB = Math.round(file.size / 1024);
|
||||
let fileSizeStr = fileSizeKB + ' KB';
|
||||
@@ -383,10 +426,11 @@
|
||||
const statusClass = `status-${file.status || 'pending'}`;
|
||||
|
||||
// Process button (only show if not completed/processing)
|
||||
const processButton = file.status === 'completed' || file.status === 'processing'
|
||||
const processButton = file.status === 'completed' || file.status === 'processing' || file.status === 'queued'
|
||||
? ''
|
||||
: `<button class="process-btn" onclick="triggerProcessing('${file.name}')">Process</button>`;
|
||||
|
||||
|
||||
// Transcript indicator
|
||||
const transcriptInfo = file.has_transcript
|
||||
? '<span class="transcript-available">✓ Transcript</span>'
|
||||
@@ -426,19 +470,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
// New function to process multiple files at once
|
||||
async function processAllSelected() {
|
||||
const selectedFiles = document.querySelectorAll('.file-item input[type="checkbox"]:checked');
|
||||
if (selectedFiles.length === 0) {
|
||||
showResult('Please select at least one file to process', false);
|
||||
// Function to process all pending files
|
||||
async function processAllPending() {
|
||||
// Get all files with pending or failed status
|
||||
const allFiles = Array.from(document.querySelectorAll('.file-item'));
|
||||
const pendingFiles = allFiles
|
||||
.filter(item => {
|
||||
const statusBadge = item.querySelector('.status-badge');
|
||||
return statusBadge &&
|
||||
(statusBadge.classList.contains('status-pending') ||
|
||||
statusBadge.classList.contains('status-failed'));
|
||||
})
|
||||
.map(item => {
|
||||
// Extract filename from the file-name div
|
||||
return item.querySelector('.file-name').textContent;
|
||||
});
|
||||
|
||||
if (pendingFiles.length === 0) {
|
||||
showResult('No pending files to process', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each file
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const checkbox of selectedFiles) {
|
||||
const filename = checkbox.getAttribute('data-filename');
|
||||
for (const filename of pendingFiles) {
|
||||
try {
|
||||
const response = await fetch(`/process/${filename}`, {
|
||||
method: 'POST'
|
||||
@@ -457,6 +514,37 @@
|
||||
showResult(`Processing started for ${successCount} file(s). ${failCount > 0 ? failCount + ' file(s) failed.' : ''}`, failCount === 0);
|
||||
}
|
||||
|
||||
// Function to download all transcripts
|
||||
async function downloadAllTranscripts() {
|
||||
const downloadBtn = document.getElementById('downloadAllBtn');
|
||||
|
||||
// Check if button is disabled
|
||||
if (downloadBtn.classList.contains('disabled-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger file download
|
||||
window.location.href = '/download-transcripts';
|
||||
}
|
||||
|
||||
// Function to update the download button state
|
||||
function updateDownloadButtonState(files) {
|
||||
const downloadBtn = document.getElementById('downloadAllBtn');
|
||||
|
||||
// Check if any files have transcripts
|
||||
const hasTranscripts = files ? files.some(file => file.has_transcript) : false;
|
||||
|
||||
if (hasTranscripts) {
|
||||
downloadBtn.classList.remove('disabled-btn');
|
||||
} else {
|
||||
downloadBtn.classList.add('disabled-btn');
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listeners for both buttons
|
||||
document.getElementById('processAllBtn').addEventListener('click', processAllPending);
|
||||
document.getElementById('downloadAllBtn').addEventListener('click', downloadAllTranscripts);
|
||||
|
||||
// Initialize WebSocket connection when page loads
|
||||
document.addEventListener('DOMContentLoaded', connectWebSocket);
|
||||
</script>
|
||||
46
src/main.py
46
src/main.py
@@ -5,10 +5,12 @@ from contextlib import asynccontextmanager
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI, UploadFile, File, HTTPException, Request, WebSocket, WebSocketDisconnect, Form
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from loguru import logger
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
from config import UPLOAD_DIR, ALLOWED_EXTENSIONS, STATIC_DIR, TEMPLATES_DIR, TRANSCRIPT_DIR
|
||||
from worker import FileStatus, AudioProcessor
|
||||
@@ -68,7 +70,6 @@ async def upload_file(file: UploadFile = File(...)):
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Broadcast file list update to all connected clients
|
||||
import asyncio
|
||||
asyncio.create_task(broadcast_file_list())
|
||||
|
||||
return {"filename": file.filename, "saved_path": str(file_path), "status": "success"}
|
||||
@@ -195,7 +196,7 @@ def get_file_list():
|
||||
"""Helper function to get file list with metadata and status"""
|
||||
files = []
|
||||
for file_path in UPLOAD_DIR.iterdir():
|
||||
if file_path.is_file():
|
||||
if file_path.is_file() and file_path.name != '.gitkeep': # Skip .gitkeep file
|
||||
filename = file_path.name
|
||||
file_stats = file_path.stat()
|
||||
|
||||
@@ -243,6 +244,39 @@ async def process_file(filename: str):
|
||||
return {"filename": filename, "status": "processing_started"}
|
||||
|
||||
|
||||
@app.get("/download-transcripts")
|
||||
async def download_transcripts():
|
||||
"""
|
||||
API endpoint to download all transcript files as a single ZIP
|
||||
"""
|
||||
# Check if there are any transcripts
|
||||
transcript_files = list(TRANSCRIPT_DIR.glob("*.txt"))
|
||||
if not transcript_files:
|
||||
raise HTTPException(status_code=404, detail="No transcripts available")
|
||||
|
||||
# Create a ZIP file in memory
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for transcript_path in transcript_files:
|
||||
# Add each transcript to the ZIP file
|
||||
zip_file.write(
|
||||
transcript_path,
|
||||
arcname=transcript_path.name
|
||||
)
|
||||
|
||||
# Reset buffer position
|
||||
zip_buffer.seek(0)
|
||||
|
||||
# Create a streaming response with the ZIP file
|
||||
return StreamingResponse(
|
||||
zip_buffer,
|
||||
media_type="application/zip",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=transcripts.zip"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for real-time file updates"""
|
||||
@@ -267,6 +301,10 @@ async def broadcast_file_list():
|
||||
if active_connections:
|
||||
file_list = get_file_list()
|
||||
for connection in active_connections:
|
||||
await connection.send_text(json.dumps({"files": file_list}))
|
||||
try:
|
||||
await connection.send_text(json.dumps({"files": file_list}))
|
||||
except Exception as e:
|
||||
logger.error(f"Error broadcasting to a client: {e}")
|
||||
|
||||
|
||||
# Run with: uvicorn src.main:app --reload
|
||||
Reference in New Issue
Block a user