improved uiux
This commit is contained in:
123
src/main.py
123
src/main.py
@@ -2,9 +2,10 @@ import asyncio
|
||||
import shutil
|
||||
import json
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI, UploadFile, File, HTTPException, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi import FastAPI, UploadFile, File, HTTPException, Request, WebSocket, WebSocketDisconnect, Form
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from loguru import logger
|
||||
@@ -73,6 +74,123 @@ async def upload_file(file: UploadFile = File(...)):
|
||||
return {"filename": file.filename, "saved_path": str(file_path), "status": "success"}
|
||||
|
||||
|
||||
@app.post("/upload-multiple")
|
||||
async def upload_multiple_files(files: List[UploadFile] = File(...)):
|
||||
"""
|
||||
API endpoint to handle multiple file uploads
|
||||
- Processes each file individually
|
||||
- Returns a summary of the upload results
|
||||
"""
|
||||
results = []
|
||||
|
||||
for file in files:
|
||||
# Validate file extension
|
||||
if not is_valid_file(file.filename):
|
||||
results.append({
|
||||
"filename": file.filename,
|
||||
"status": "error",
|
||||
"detail": f"Invalid file type. Only {', '.join(ALLOWED_EXTENSIONS)} are allowed."
|
||||
})
|
||||
continue
|
||||
|
||||
# Check if file already exists
|
||||
file_path = UPLOAD_DIR / file.filename
|
||||
if file_path.exists():
|
||||
results.append({
|
||||
"filename": file.filename,
|
||||
"status": "duplicate",
|
||||
"detail": "File already exists"
|
||||
})
|
||||
continue
|
||||
|
||||
# Save the file
|
||||
try:
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
results.append({
|
||||
"filename": file.filename,
|
||||
"saved_path": str(file_path),
|
||||
"status": "success"
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving file {file.filename}: {str(e)}")
|
||||
results.append({
|
||||
"filename": file.filename,
|
||||
"status": "error",
|
||||
"detail": f"Failed to save file: {str(e)}"
|
||||
})
|
||||
|
||||
# Broadcast file list update to all connected clients
|
||||
asyncio.create_task(broadcast_file_list())
|
||||
|
||||
# Calculate summary
|
||||
summary = {
|
||||
"total": len(files),
|
||||
"success": sum(1 for r in results if r["status"] == "success"),
|
||||
"duplicate": sum(1 for r in results if r["status"] == "duplicate"),
|
||||
"error": sum(1 for r in results if r["status"] == "error"),
|
||||
"results": results
|
||||
}
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
@app.post("/process-multiple")
|
||||
async def process_multiple_files(filenames: List[str] = Form(...)):
|
||||
"""
|
||||
API endpoint to process multiple files at once
|
||||
"""
|
||||
results = []
|
||||
|
||||
for filename in filenames:
|
||||
file_path = UPLOAD_DIR / filename
|
||||
|
||||
# Check if file exists
|
||||
if not file_path.exists():
|
||||
results.append({
|
||||
"filename": filename,
|
||||
"status": "error",
|
||||
"detail": "File not found"
|
||||
})
|
||||
continue
|
||||
|
||||
# Process the file
|
||||
try:
|
||||
success = await audio_processor.process_file(filename)
|
||||
if success:
|
||||
results.append({
|
||||
"filename": filename,
|
||||
"status": "processing_started"
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
"filename": filename,
|
||||
"status": "error",
|
||||
"detail": "Failed to start processing"
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing file {filename}: {str(e)}")
|
||||
results.append({
|
||||
"filename": filename,
|
||||
"status": "error",
|
||||
"detail": f"Error: {str(e)}"
|
||||
})
|
||||
|
||||
# Broadcast updated status
|
||||
await broadcast_file_list()
|
||||
|
||||
# Calculate summary
|
||||
summary = {
|
||||
"total": len(filenames),
|
||||
"success": sum(1 for r in results if r["status"] == "processing_started"),
|
||||
"error": sum(1 for r in results if r["status"] == "error"),
|
||||
"results": results
|
||||
}
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def get_file_list():
|
||||
"""Helper function to get file list with metadata and status"""
|
||||
files = []
|
||||
@@ -124,6 +242,7 @@ async def process_file(filename: str):
|
||||
|
||||
return {"filename": filename, "status": "processing_started"}
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for real-time file updates"""
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Head content remains the same -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Transcriptor Agent</title>
|
||||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
|
||||
<style>
|
||||
/* Existing styles */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
@@ -67,20 +69,24 @@
|
||||
margin-top: 15px;
|
||||
}
|
||||
.file-item {
|
||||
padding: 8px;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.file-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.file-name {
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
color: #444;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.file-meta {
|
||||
color: #777;
|
||||
font-size: 0.9em;
|
||||
color: #888;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.connection-status {
|
||||
font-size: 0.8em;
|
||||
@@ -101,35 +107,87 @@
|
||||
background-color: #fcf8e3;
|
||||
color: #8a6d3b;
|
||||
}
|
||||
.file-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
white-space: nowrap;
|
||||
min-width: 180px;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
margin-right: 8px;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
.status-pending { background-color: #fcf8e3; color: #8a6d3b; }
|
||||
.status-queued { background-color: #f0f8ff; color: #5588bb; }
|
||||
.status-processing { background-color: #d9edf7; color: #31708f; }
|
||||
.status-completed { background-color: #dff0d8; color: #3c763d; }
|
||||
.status-failed { background-color: #f2dede; color: #a94442; }
|
||||
.process-btn {
|
||||
background-color: #5bc0de;
|
||||
padding: 3px 8px;
|
||||
font-size: 0.8em;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.85em;
|
||||
border-radius: 4px;
|
||||
min-width: 70px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
.transcript-available {
|
||||
color: #3c763d;
|
||||
font-size: 0.85em;
|
||||
margin-right: 8px;
|
||||
}
|
||||
/* New styles for upload progress */
|
||||
.upload-progress {
|
||||
margin-top: 15px;
|
||||
display: none;
|
||||
}
|
||||
.progress-item {
|
||||
margin-bottom: 6px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.progress-filename {
|
||||
font-weight: normal;
|
||||
color: #444;
|
||||
max-width: 75%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.progress-status {
|
||||
color: #777;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Transcriptor Agent</h1>
|
||||
|
||||
|
||||
<div class="upload-form">
|
||||
<form id="uploadForm">
|
||||
<div class="form-group">
|
||||
<label for="audioFile">Select audio file:</label>
|
||||
<input type="file" id="audioFile" name="file" accept=".mp3,.wav" required>
|
||||
<label for="audioFile">Select audio files (MP3, WAV):</label>
|
||||
<input type="file" id="audioFile" name="files" accept=".mp3,.wav" multiple required>
|
||||
</div>
|
||||
<button type="submit">Upload</button>
|
||||
<button type="submit">Upload Files</button>
|
||||
</form>
|
||||
|
||||
<!-- New progress container -->
|
||||
<div id="uploadProgress" class="upload-progress">
|
||||
<h3>Upload Progress</h3>
|
||||
<div id="progressItems"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result" style="display: none;"></div>
|
||||
@@ -148,49 +206,93 @@
|
||||
e.preventDefault();
|
||||
|
||||
const fileInput = document.getElementById('audioFile');
|
||||
const file = fileInput.files[0];
|
||||
const files = fileInput.files;
|
||||
|
||||
if (!file) {
|
||||
showResult('Please select a file.', false);
|
||||
if (!files || files.length === 0) {
|
||||
showResult('Please select at least one file.', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
const fileName = file.name;
|
||||
const fileExt = fileName.split('.').pop().toLowerCase();
|
||||
// Setup progress tracking
|
||||
const progressContainer = document.getElementById('uploadProgress');
|
||||
const progressItems = document.getElementById('progressItems');
|
||||
progressContainer.style.display = 'block';
|
||||
progressItems.innerHTML = '';
|
||||
|
||||
if (!['mp3', 'wav'].includes(fileExt)) {
|
||||
showResult('Only MP3 and WAV files are allowed.', false);
|
||||
return;
|
||||
}
|
||||
// Keep track of successful and failed uploads
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
let duplicateCount = 0;
|
||||
|
||||
// Create form data
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
// Process each file
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
try {
|
||||
const response = await fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
// Create progress item
|
||||
const progressId = `progress-${i}`;
|
||||
const progressItem = document.createElement('div');
|
||||
progressItem.className = 'progress-item';
|
||||
progressItem.innerHTML = `
|
||||
<span class="progress-filename">${file.name}</span>
|
||||
<span id="${progressId}" class="progress-status">Uploading...</span>
|
||||
`;
|
||||
progressItems.appendChild(progressItem);
|
||||
|
||||
const result = await response.json();
|
||||
// Check file extension
|
||||
const fileName = file.name;
|
||||
const fileExt = fileName.split('.').pop().toLowerCase();
|
||||
|
||||
if (response.ok) {
|
||||
if (result.status === "duplicate") {
|
||||
showResult(`File "${result.filename}" already exists in uploads directory!`, false);
|
||||
} else {
|
||||
showResult(`File "${result.filename}" uploaded successfully!`, true);
|
||||
fileInput.value = ''; // Clear the input
|
||||
|
||||
// No need to manually refresh - WebSocket will handle it
|
||||
}
|
||||
} else {
|
||||
showResult(`Error: ${result.detail}`, false);
|
||||
if (!['mp3', 'wav'].includes(fileExt)) {
|
||||
document.getElementById(progressId).textContent = 'Invalid format (MP3/WAV only)';
|
||||
document.getElementById(progressId).style.color = '#a94442';
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create form data for this file
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (result.status === "duplicate") {
|
||||
document.getElementById(progressId).textContent = 'Already exists';
|
||||
document.getElementById(progressId).style.color = '#8a6d3b';
|
||||
duplicateCount++;
|
||||
} else {
|
||||
document.getElementById(progressId).textContent = 'Uploaded successfully';
|
||||
document.getElementById(progressId).style.color = '#3c763d';
|
||||
successCount++;
|
||||
}
|
||||
} else {
|
||||
document.getElementById(progressId).textContent = `Error: ${result.detail}`;
|
||||
document.getElementById(progressId).style.color = '#a94442';
|
||||
failCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById(progressId).textContent = `Failed: ${error.message}`;
|
||||
document.getElementById(progressId).style.color = '#a94442';
|
||||
failCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
showResult(`Upload failed: ${error.message}`, false);
|
||||
}
|
||||
|
||||
// Show summary
|
||||
let summary = "";
|
||||
if (successCount > 0) summary += `${successCount} file(s) uploaded successfully. `;
|
||||
if (duplicateCount > 0) summary += `${duplicateCount} file(s) already exist. `;
|
||||
if (failCount > 0) summary += `${failCount} file(s) failed to upload.`;
|
||||
|
||||
showResult(summary, successCount > 0 && failCount === 0);
|
||||
|
||||
// Clear the input
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
function showResult(message, isSuccess) {
|
||||
@@ -256,73 +358,104 @@
|
||||
}
|
||||
|
||||
function updateFileList(files) {
|
||||
const fileListElement = document.getElementById('fileList');
|
||||
const fileListElement = document.getElementById('fileList');
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
fileListElement.innerHTML = '<p>No files uploaded yet.</p>';
|
||||
return;
|
||||
}
|
||||
if (!files || files.length === 0) {
|
||||
fileListElement.innerHTML = '<p>No files uploaded yet.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
files.forEach(file => {
|
||||
// Format the file size
|
||||
const fileSizeKB = Math.round(file.size / 1024);
|
||||
let fileSizeStr = fileSizeKB + ' KB';
|
||||
if (fileSizeKB >= 1024) {
|
||||
const fileSizeMB = (fileSizeKB / 1024).toFixed(1);
|
||||
fileSizeStr = fileSizeMB + ' MB';
|
||||
}
|
||||
let html = '';
|
||||
files.forEach(file => {
|
||||
// Format the file size
|
||||
const fileSizeKB = Math.round(file.size / 1024);
|
||||
let fileSizeStr = fileSizeKB + ' KB';
|
||||
if (fileSizeKB >= 1024) {
|
||||
const fileSizeMB = (fileSizeKB / 1024).toFixed(1);
|
||||
fileSizeStr = fileSizeMB + ' MB';
|
||||
}
|
||||
|
||||
// Format the date
|
||||
const date = new Date(file.created * 1000);
|
||||
const dateStr = date.toLocaleString();
|
||||
// Format the date
|
||||
const date = new Date(file.created * 1000);
|
||||
const dateStr = date.toLocaleString();
|
||||
|
||||
// Status badge
|
||||
const statusClass = `status-${file.status || 'pending'}`;
|
||||
// Status badge
|
||||
const statusClass = `status-${file.status || 'pending'}`;
|
||||
|
||||
// Process button (only show if not completed/processing)
|
||||
const processButton = file.status === 'completed' || file.status === 'processing'
|
||||
? ''
|
||||
: `<button class="process-btn" onclick="triggerProcessing('${file.name}')">Process</button>`;
|
||||
// Process button (only show if not completed/processing)
|
||||
const processButton = file.status === 'completed' || file.status === 'processing'
|
||||
? ''
|
||||
: `<button class="process-btn" onclick="triggerProcessing('${file.name}')">Process</button>`;
|
||||
|
||||
// Transcript indicator
|
||||
const transcriptInfo = file.has_transcript
|
||||
? '<span class="transcript-available">✓ Transcript</span>'
|
||||
: '';
|
||||
// Transcript indicator
|
||||
const transcriptInfo = file.has_transcript
|
||||
? '<span class="transcript-available">✓ Transcript</span>'
|
||||
: '';
|
||||
|
||||
html += `<div class="file-item">
|
||||
<div>
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-meta">Size: ${fileSizeStr} | Uploaded: ${dateStr}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="status-badge ${statusClass}">${file.status || 'pending'}</span>
|
||||
${transcriptInfo}
|
||||
${processButton}
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
fileListElement.innerHTML = html;
|
||||
}
|
||||
|
||||
// Add this function after updateFileList
|
||||
async function triggerProcessing(filename) {
|
||||
try {
|
||||
const response = await fetch(`/process/${filename}`, {
|
||||
method: 'POST'
|
||||
html += `<div class="file-item">
|
||||
<div>
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-meta">Size: ${fileSizeStr} | Uploaded: ${dateStr}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="status-badge ${statusClass}">${file.status || 'pending'}</span>
|
||||
${transcriptInfo}
|
||||
${processButton}
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showResult(`Processing started for "${filename}"`, true);
|
||||
} else {
|
||||
const result = await response.json();
|
||||
showResult(`Error: ${result.detail}`, false);
|
||||
}
|
||||
} catch (error) {
|
||||
showResult(`Processing request failed: ${error.message}`, false);
|
||||
fileListElement.innerHTML = html;
|
||||
}
|
||||
|
||||
// Add this function after updateFileList
|
||||
async function triggerProcessing(filename) {
|
||||
try {
|
||||
const response = await fetch(`/process/${filename}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showResult(`Processing started for "${filename}"`, true);
|
||||
} else {
|
||||
const result = await response.json();
|
||||
showResult(`Error: ${result.detail}`, false);
|
||||
}
|
||||
} catch (error) {
|
||||
showResult(`Processing request failed: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const checkbox of selectedFiles) {
|
||||
const filename = checkbox.getAttribute('data-filename');
|
||||
try {
|
||||
const response = await fetch(`/process/${filename}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
showResult(`Processing started for ${successCount} file(s). ${failCount > 0 ? failCount + ' file(s) failed.' : ''}`, failCount === 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize WebSocket connection when page loads
|
||||
document.addEventListener('DOMContentLoaded', connectWebSocket);
|
||||
|
||||
106
src/worker.py
106
src/worker.py
@@ -3,8 +3,9 @@ import asyncio
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set, Optional
|
||||
from typing import Dict, List, Set, Optional, Deque
|
||||
from enum import Enum
|
||||
from collections import deque
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -14,6 +15,7 @@ from engine.stt import WhisperEngine, TranscriptionResult
|
||||
|
||||
class FileStatus(Enum):
|
||||
PENDING = "pending"
|
||||
QUEUED = "queued"
|
||||
PROCESSING = "processing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
@@ -24,7 +26,9 @@ class AudioProcessor:
|
||||
self.file_status: Dict[str, FileStatus] = {}
|
||||
self.is_running = False
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._transcription_tasks: Dict[str, asyncio.Task] = {}
|
||||
self._processing_queue: Deque[str] = deque()
|
||||
self._processing_lock = asyncio.Lock()
|
||||
self._queue_processor_task: Optional[asyncio.Task] = None
|
||||
|
||||
# Initialize Whisper engine
|
||||
self.stt_engine = WhisperEngine()
|
||||
@@ -53,6 +57,7 @@ class AudioProcessor:
|
||||
if not self._task or self._task.done():
|
||||
self.is_running = True
|
||||
self._task = asyncio.create_task(self._monitor_files())
|
||||
self._queue_processor_task = asyncio.create_task(self._process_queue())
|
||||
logger.info(f"STT Engine started with model: {self.stt_engine.model_name}")
|
||||
|
||||
def stop(self):
|
||||
@@ -61,11 +66,8 @@ class AudioProcessor:
|
||||
if self._task and not self._task.done():
|
||||
self._task.cancel()
|
||||
|
||||
# Cancel any running transcription tasks
|
||||
for filename, task in self._transcription_tasks.items():
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
logger.info(f"Cancelled transcription task for {filename}")
|
||||
if self._queue_processor_task and not self._queue_processor_task.done():
|
||||
self._queue_processor_task.cancel()
|
||||
|
||||
async def _monitor_files(self):
|
||||
"""Monitor the uploads folder for new files"""
|
||||
@@ -90,21 +92,41 @@ class AudioProcessor:
|
||||
logger.error(f"File not found: {file_path}")
|
||||
return False
|
||||
|
||||
# Check if already processing
|
||||
if filename in self._transcription_tasks and not self._transcription_tasks[filename].done():
|
||||
logger.info(f"File {filename} is already being processed")
|
||||
# Check if already in queue or processing
|
||||
current_status = self.file_status.get(filename, FileStatus.PENDING)
|
||||
if current_status in [FileStatus.QUEUED, FileStatus.PROCESSING]:
|
||||
logger.info(f"File {filename} is already in queue or being processed")
|
||||
return True
|
||||
|
||||
# Update status
|
||||
self.file_status[filename] = FileStatus.PROCESSING
|
||||
# Update status to queued
|
||||
self.file_status[filename] = FileStatus.QUEUED
|
||||
|
||||
# Create transcription task
|
||||
self._transcription_tasks[filename] = asyncio.create_task(
|
||||
self._transcribe_file(filename)
|
||||
)
|
||||
# Add to processing queue
|
||||
self._processing_queue.append(filename)
|
||||
logger.info(f"Added {filename} to processing queue. Queue size: {len(self._processing_queue)}")
|
||||
|
||||
return True
|
||||
|
||||
async def _process_queue(self):
|
||||
"""Process files in the queue sequentially"""
|
||||
while self.is_running:
|
||||
if self._processing_queue:
|
||||
# Get the next file to process
|
||||
filename = self._processing_queue.popleft()
|
||||
|
||||
# Process the file
|
||||
self.file_status[filename] = FileStatus.PROCESSING
|
||||
logger.info(f"Processing file from queue: {filename}")
|
||||
|
||||
try:
|
||||
await self._transcribe_file(filename)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing queued file {filename}: {e}")
|
||||
self.file_status[filename] = FileStatus.FAILED
|
||||
|
||||
# Sleep briefly before checking the queue again
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def _transcribe_file(self, filename: str) -> bool:
|
||||
"""Run the actual transcription in a separate task."""
|
||||
file_path = UPLOAD_DIR / filename
|
||||
@@ -115,32 +137,34 @@ class AudioProcessor:
|
||||
try:
|
||||
logger.info(f"Starting transcription for {filename}")
|
||||
|
||||
# Run transcription (in a thread pool to avoid blocking the event loop)
|
||||
result = await asyncio.to_thread(
|
||||
self.stt_engine.transcribe,
|
||||
file_path
|
||||
)
|
||||
# Ensure only one transcription runs at a time
|
||||
async with self._processing_lock:
|
||||
# Run transcription (in a thread pool to avoid blocking the event loop)
|
||||
result = await asyncio.to_thread(
|
||||
self.stt_engine.transcribe,
|
||||
file_path
|
||||
)
|
||||
|
||||
# Save plain text transcript
|
||||
with open(transcript_path, "w", encoding="utf-8") as f:
|
||||
f.write(result.text)
|
||||
# Save plain text transcript
|
||||
with open(transcript_path, "w", encoding="utf-8") as f:
|
||||
f.write(result.text)
|
||||
|
||||
# Save detailed JSON result
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
# Create a serializable version of the result
|
||||
serializable_result = {
|
||||
"text": result.text,
|
||||
"language": result.language,
|
||||
"segments": result.segments,
|
||||
"duration": result.duration,
|
||||
"processing_time": result.processing_time,
|
||||
"metadata": {
|
||||
"model": self.stt_engine.model_name,
|
||||
"device": self.stt_engine.device,
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
# Save detailed JSON result
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
# Create a serializable version of the result
|
||||
serializable_result = {
|
||||
"text": result.text,
|
||||
"language": result.language,
|
||||
"segments": result.segments,
|
||||
"duration": result.duration,
|
||||
"processing_time": result.processing_time,
|
||||
"metadata": {
|
||||
"model": self.stt_engine.model_name,
|
||||
"device": self.stt_engine.device,
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
}
|
||||
}
|
||||
json.dump(serializable_result, f, indent=2)
|
||||
json.dump(serializable_result, f, indent=2)
|
||||
|
||||
logger.success(f"Transcription completed for {filename} "
|
||||
f"({result.duration:.1f}s audio, {result.processing_time:.1f}s processing)")
|
||||
@@ -160,6 +184,4 @@ class AudioProcessor:
|
||||
|
||||
def get_all_statuses(self) -> Dict[str, str]:
|
||||
"""Get status of all files"""
|
||||
return {name: status.value for name, status in self.file_status.items()}
|
||||
|
||||
|
||||
return {name: status.value for name, status in self.file_status.items()}
|
||||
Reference in New Issue
Block a user