add transcription history logging

This commit is contained in:
Serge Zaigraeff
2025-03-04 09:55:47 +03:00
parent 450750c47b
commit 8dab730d4a
8 changed files with 887 additions and 167 deletions
+2
View File
@@ -1,2 +1,4 @@
.DS_Store
__pycache__
models
logs
+85
View File
@@ -0,0 +1,85 @@
"""
Модуль history_logger.py содержит класс HistoryLogger для журналирования результатов
транскрибации.
"""
import os
import json
import datetime
import random
import string
from typing import Dict, Any, Optional
from .utils import logger
class HistoryLogger:
"""Класс для сохранения истории транскрибации."""
def __init__(self, config: Dict):
"""
Инициализация логгера истории.
Args:
config: Словарь с конфигурацией.
"""
self.config = config
self.history_enabled = config.get("enable_history", False)
self.history_root = os.path.join(os.path.dirname(os.path.dirname(__file__)), "history")
# Создаем корневую директорию истории, если она не существует
if self.history_enabled and not os.path.exists(self.history_root):
os.makedirs(self.history_root)
logger.info(f"Создана директория для истории транскрибации: {self.history_root}")
def save(self, result: Dict[str, Any], original_filename: str) -> Optional[str]:
"""
Сохраняет результат транскрибации в файл истории.
Args:
result: Результат транскрибации.
original_filename: Исходное имя аудиофайла.
Returns:
Путь к сохраненному файлу истории или None, если сохранение отключено.
"""
if not self.history_enabled:
logger.debug("История транскрибации отключена в конфигурации")
return None
try:
# Получаем текущую дату и время
now = datetime.datetime.now()
date_str = now.strftime("%Y-%m-%d")
# Получаем текущий таймстамп в миллисекундах
timestamp_ms = int(datetime.datetime.now().timestamp() * 1000)
# Генерируем 4-символьную случайную метку
random_tag = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
# Получаем только имя файла без пути
base_filename = os.path.basename(original_filename)
# Создаем имя файла истории
history_filename = f"{timestamp_ms}_{base_filename}_{random_tag}.json"
# Путь к директории для текущей даты
date_dir = os.path.join(self.history_root, date_str)
# Создаем директорию для текущей даты, если она не существует
if not os.path.exists(date_dir):
os.makedirs(date_dir)
# Полный путь к файлу истории
history_path = os.path.join(date_dir, history_filename)
# Сохраняем результат в JSON файл
with open(history_path, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
logger.info(f"Результат транскрибации сохранен в историю: {history_path}")
return history_path
except Exception as e:
logger.error(f"Ошибка при сохранении истории транскрибации: {e}")
return None
+14 -140
View File
@@ -1,19 +1,14 @@
"""
Модуль routes.py содержит классы для обработки транскрибации аудиофайлов
и регистрации маршрутов API для сервиса распознавания речи.
Модуль routes.py содержит классы для регистрации маршрутов API
для сервиса распознавания речи.
"""
import os
import uuid
import tempfile
import time
import librosa
from flask import request, jsonify
from typing import Dict, Tuple
from typing import Dict
from .utils import logger
from .transcriber_service import TranscriptionService
from .audio_sources import (
AudioSource,
UploadedFileSource,
URLSource,
Base64Source,
@@ -21,131 +16,6 @@ from .audio_sources import (
)
class TranscriptionService:
"""Сервис для обработки и транскрибации аудиофайлов."""
def __init__(self, transcriber, config: Dict):
"""
Инициализация сервиса транскрибации.
Args:
transcriber: Экземпляр транскрайбера.
config: Словарь с конфигурацией.
"""
self.transcriber = transcriber
self.config = config
self.max_file_size_mb = self.config.get("max_file_size", 100) # Default 100MB
def get_audio_duration(self, file_path: str) -> float:
"""
Определяет длительность аудиофайла в секундах.
Args:
file_path: Путь к аудиофайлу.
Returns:
Длительность в секундах.
"""
try:
y, sr = librosa.load(file_path, sr=None)
duration = librosa.get_duration(y=y, sr=sr)
return duration
except Exception as e:
logger.error(f"Ошибка при определении длительности файла: {e}")
return 0.0
def transcribe_from_source(self, source: AudioSource, params: Dict = None) -> Tuple[Dict, int]:
"""
Транскрибирует аудиофайл из указанного источника.
Args:
source: Источник аудиофайла.
params: Дополнительные параметры для транскрибации.
Returns:
Кортеж (JSON-ответ, HTTP-код).
"""
# Получаем файл из источника
file, filename, error = source.get_audio_file()
# Обрабатываем ошибки получения файла
if error:
return jsonify({"error": error}), 400
if not file:
return jsonify({"error": "Failed to get audio file"}), 400
# Извлекаем параметры из запроса, если они есть
params = params or {}
language = params.get('language', self.config.get('language', 'en'))
temperature = float(params.get('temperature', 0.0))
prompt = params.get('prompt', '')
# Проверяем, запрошены ли временные метки
return_timestamps = params.get('return_timestamps', self.config.get('return_timestamps', False))
# Преобразуем строковое значение в булево, если необходимо
if isinstance(return_timestamps, str):
return_timestamps = return_timestamps.lower() in ('true', 't', 'yes', 'y', '1')
# Временно изменяем настройку return_timestamps в транскрайбере
original_return_timestamps = self.transcriber.return_timestamps
self.transcriber.return_timestamps = return_timestamps
# Сохраняем файл во временный файл
temp_dir = tempfile.mkdtemp()
temp_file_path = os.path.join(temp_dir, str(uuid.uuid4()) + "_" + os.path.basename(filename))
file.save(temp_file_path)
# Определяем длительность аудиофайла
duration = self.get_audio_duration(temp_file_path)
# Для файлов из внешних источников (URL, base64), закрываем их и выполняем очистку
if hasattr(source, 'cleanup'):
file.file.close() # Закрываем файловый объект
source.cleanup() # Очищаем временные файлы источника
try:
start_time = time.time()
result = self.transcriber.process_file(temp_file_path)
processing_time = time.time() - start_time
# Формируем ответ в зависимости от return_timestamps
if return_timestamps:
response = {
"segments": result.get("segments", []),
"text": result.get("text", ""),
"processing_time": processing_time,
"response_size_bytes": len(str(result).encode('utf-8')),
"duration_seconds": duration,
"model": os.path.basename(self.config["model_path"])
}
else:
# Если не запрашивались временные метки, result - это строка
response = {
"text": result,
"processing_time": processing_time,
"response_size_bytes": len(str(result).encode('utf-8')),
"duration_seconds": duration,
"model": os.path.basename(self.config["model_path"])
}
return jsonify(response), 200
except Exception as e:
logger.error(f"Ошибка при транскрибации: {e}")
return jsonify({"error": str(e)}), 500
finally:
# Восстанавливаем оригинальное значение return_timestamps
self.transcriber.return_timestamps = original_return_timestamps
# Очистка временных файлов
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
if os.path.exists(temp_dir):
os.rmdir(temp_dir)
class Routes:
"""Класс для регистрации всех эндпоинтов API."""
@@ -191,8 +61,8 @@ class Routes:
file_path = data["file_path"]
source = LocalFileSource(file_path, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, data)
response, status_code = self.transcription_service.transcribe_from_source(source, data)
return jsonify(response), status_code
@self.app.route('/v1/models', methods=['GET'])
def list_models():
@@ -229,7 +99,8 @@ class Routes:
def openai_transcribe_endpoint():
"""Эндпоинт для транскрибации аудиофайла (multipart-форма)."""
source = UploadedFileSource(request.files, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, request.form)
response, status_code = self.transcription_service.transcribe_from_source(source, request.form)
return jsonify(response), status_code
@self.app.route('/v1/audio/transcriptions/url', methods=['POST'])
def transcribe_from_url():
@@ -247,7 +118,8 @@ class Routes:
params = {k: v for k, v in data.items() if k != "url"}
source = URLSource(url, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, params)
response, status_code = self.transcription_service.transcribe_from_source(source, params)
return jsonify(response), status_code
@self.app.route('/v1/audio/transcriptions/base64', methods=['POST'])
def transcribe_from_base64():
@@ -265,10 +137,12 @@ class Routes:
params = {k: v for k, v in data.items() if k != "file"}
source = Base64Source(base64_data, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, params)
response, status_code = self.transcription_service.transcribe_from_source(source, params)
return jsonify(response), status_code
@self.app.route('/v1/audio/transcriptions/multipart', methods=['POST'])
def transcribe_multipart():
"""Эндпоинт для транскрибации аудиофайла, загруженного через форму."""
source = UploadedFileSource(request.files, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, request.form)
response, status_code = self.transcription_service.transcribe_from_source(source, request.form)
return jsonify(response), status_code
+146
View File
@@ -0,0 +1,146 @@
"""
Модуль transcriber_service.py содержит класс TranscriptionService,
который отвечает за обработку и транскрибацию аудиофайлов.
"""
import os
import uuid
import tempfile
import time
import librosa
from typing import Dict, Tuple
from .utils import logger
from .history_logger import HistoryLogger
from .audio_sources import AudioSource
class TranscriptionService:
"""Сервис для обработки и транскрибации аудиофайлов."""
def __init__(self, transcriber, config: Dict):
"""
Инициализация сервиса транскрибации.
Args:
transcriber: Экземпляр транскрайбера.
config: Словарь с конфигурацией.
"""
self.transcriber = transcriber
self.config = config
self.max_file_size_mb = self.config.get("max_file_size", 100) # Default 100MB
# Объект журналирования
self.history = HistoryLogger(config)
def get_audio_duration(self, file_path: str) -> float:
"""
Определяет длительность аудиофайла в секундах.
Args:
file_path: Путь к аудиофайлу.
Returns:
Длительность в секундах.
"""
try:
y, sr = librosa.load(file_path, sr=None)
duration = librosa.get_duration(y=y, sr=sr)
return duration
except Exception as e:
logger.error(f"Ошибка при определении длительности файла: {e}")
return 0.0
def transcribe_from_source(self, source: AudioSource, params: Dict = None) -> Tuple[Dict, int]:
"""
Транскрибирует аудиофайл из указанного источника.
Args:
source: Источник аудиофайла.
params: Дополнительные параметры для транскрибации.
Returns:
Кортеж (JSON-ответ, HTTP-код).
"""
# Получаем файл из источника
file, filename, error = source.get_audio_file()
# Обрабатываем ошибки получения файла
if error:
return {"error": error}, 400
if not file:
return {"error": "Failed to get audio file"}, 400
# Извлекаем параметры из запроса, если они есть
params = params or {}
language = params.get('language', self.config.get('language', 'en'))
temperature = float(params.get('temperature', 0.0))
prompt = params.get('prompt', '')
# Проверяем, запрошены ли временные метки
return_timestamps = params.get('return_timestamps', self.config.get('return_timestamps', False))
# Преобразуем строковое значение в булево, если необходимо
if isinstance(return_timestamps, str):
return_timestamps = return_timestamps.lower() in ('true', 't', 'yes', 'y', '1')
# Временно изменяем настройку return_timestamps в транскрайбере
original_return_timestamps = self.transcriber.return_timestamps
self.transcriber.return_timestamps = return_timestamps
# Сохраняем файл во временный файл
temp_dir = tempfile.mkdtemp()
temp_file_path = os.path.join(temp_dir, str(uuid.uuid4()) + "_" + os.path.basename(filename))
file.save(temp_file_path)
# Определяем длительность аудиофайла
duration = self.get_audio_duration(temp_file_path)
# Для файлов из внешних источников (URL, base64), закрываем их и выполняем очистку
if hasattr(source, 'cleanup'):
file.file.close() # Закрываем файловый объект
source.cleanup() # Очищаем временные файлы источника
try:
start_time = time.time()
result = self.transcriber.process_file(temp_file_path)
processing_time = time.time() - start_time
# Формируем ответ в зависимости от return_timestamps
if return_timestamps:
response = {
"segments": result.get("segments", []),
"text": result.get("text", ""),
"processing_time": processing_time,
"response_size_bytes": len(str(result).encode('utf-8')),
"duration_seconds": duration,
"model": os.path.basename(self.config["model_path"])
}
else:
# Если не запрашивались временные метки, result - это строка
response = {
"text": result,
"processing_time": processing_time,
"response_size_bytes": len(str(result).encode('utf-8')),
"duration_seconds": duration,
"model": os.path.basename(self.config["model_path"])
}
# Журналирование результата
self.history.save(response, filename)
return response, 200
except Exception as e:
logger.error(f"Ошибка при транскрибации: {e}")
return {"error": str(e)}, 500
finally:
# Восстанавливаем оригинальное значение return_timestamps
self.transcriber.return_timestamps = original_return_timestamps
# Очистка временных файлов
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
if os.path.exists(temp_dir):
os.rmdir(temp_dir)
+612
View File
@@ -0,0 +1,612 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Whisper Speech Recognition</title>
<style>
:root {
--primary-color: #3b82f6;
--primary-hover: #2563eb;
--background: #f3f4f6;
--card-bg: #ffffff;
--text-color: #1f2937;
--text-secondary: #6b7280;
--border-color: #e5e7eb;
--success-color: #10b981;
--error-color: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
background-color: var(--background);
color: var(--text-color);
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
padding: 2rem 0;
text-align: center;
}
h1 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(to right, #3b82f6, #4f46e5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
main {
flex: 1;
max-width: 1000px;
margin: 0 auto;
padding: 0 1rem;
width: 100%;
}
.container {
background-color: var(--card-bg);
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
padding: 2rem;
margin-bottom: 2rem;
}
.dropzone {
border: 2px dashed var(--border-color);
border-radius: 1rem;
padding: 3rem 2rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 1.5rem;
}
.dropzone:hover, .dropzone.dragover {
border-color: var(--primary-color);
background-color: rgba(59, 130, 246, 0.05);
}
.dropzone-icon {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
.dropzone-text {
font-size: 1.1rem;
color: var(--text-secondary);
margin-bottom: 1rem;
}
.dropzone-text strong {
color: var(--primary-color);
}
.file-info {
display: none;
background-color: rgba(59, 130, 246, 0.1);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
.file-name {
font-weight: 600;
margin-bottom: 0.5rem;
}
.file-size {
color: var(--text-secondary);
font-size: 0.9rem;
}
.btn {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 0.5rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
display: block;
width: 100%;
}
.btn:hover {
background-color: var(--primary-hover);
}
.btn:disabled {
background-color: var(--text-secondary);
cursor: not-allowed;
}
.loading {
display: none;
text-align: center;
margin: 1.5rem 0;
}
.spinner {
border: 4px solid rgba(59, 130, 246, 0.1);
border-radius: 50%;
border-top: 4px solid var(--primary-color);
width: 2rem;
height: 2rem;
animation: spin 1s linear infinite;
margin: 0 auto 0.5rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.result {
display: none;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
}
.result-title {
font-size: 1.25rem;
font-weight: 600;
}
.result-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.stat {
flex: 1;
min-width: 150px;
}
.stat-label {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.stat-value {
font-size: 1.125rem;
font-weight: 600;
}
.result-content {
background-color: var(--background);
border-radius: 0.5rem;
padding: 1.5rem;
white-space: pre-wrap;
line-height: 1.7;
max-height: 300px;
overflow-y: auto;
}
.download-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background-color: var(--success-color);
margin-top: 1rem;
}
.log-container {
display: none;
background-color: var(--card-bg);
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
padding: 2rem;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
}
.log-title {
font-size: 1.25rem;
font-weight: 600;
}
.log-content {
background-color: #1e293b;
color: #e2e8f0;
border-radius: 0.5rem;
padding: 1.5rem;
font-family: monospace;
font-size: 0.9rem;
height: 250px;
overflow-y: auto;
}
.log-entry {
margin-bottom: 0.5rem;
display: flex;
align-items: flex-start;
}
.log-time {
color: #94a3b8;
margin-right: 0.5rem;
flex-shrink: 0;
}
.log-message {
word-break: break-word;
}
.error-message {
color: var(--error-color);
margin-top: 1rem;
display: none;
}
.hidden {
display: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container, .log-container {
padding: 1.5rem;
}
.stat {
min-width: 100%;
}
.dropzone {
padding: 2rem 1rem;
}
}
</style>
</head>
<body>
<header>
<h1>Whisper Speech Recognition</h1>
</header>
<main>
<div class="container">
<div id="dropzone" class="dropzone">
<div class="dropzone-icon">🎤</div>
<div class="dropzone-text">
<strong>Перетащите аудиофайл</strong> или <strong>выберите файл</strong> для транскрибации
</div>
<div class="dropzone-text" style="font-size: 0.9rem;">
Поддерживаемые форматы: MP3, WAV, M4A, FLAC, OGG
</div>
<input type="file" id="file-input" accept="audio/*" style="display: none;">
</div>
<div id="file-info" class="file-info">
<div class="file-name" id="file-name"></div>
<div class="file-size" id="file-size"></div>
</div>
<button id="transcribe-btn" class="btn" disabled>Транскрибировать</button>
<div class="error-message" id="error-message"></div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Обрабатываем ваш аудиофайл...</p>
</div>
</div>
<div class="container result" id="result">
<div class="result-header">
<div class="result-title">Результаты транскрибации</div>
<button id="download-btn" class="btn download-btn">
<span>⬇️</span>
<span>Скачать результат</span>
</button>
</div>
<div class="result-stats">
<div class="stat">
<div class="stat-label">Длительность аудио</div>
<div class="stat-value" id="duration"></div>
</div>
<div class="stat">
<div class="stat-label">Время обработки</div>
<div class="stat-value" id="processing-time"></div>
</div>
<div class="stat">
<div class="stat-label">Модель</div>
<div class="stat-value" id="model-name"></div>
</div>
</div>
<div class="result-content" id="transcription-text"></div>
</div>
<div class="log-container" id="log-container">
<div class="log-header">
<div class="log-title">Журнал операций</div>
</div>
<div class="log-content" id="log-content"></div>
</div>
</main>
<script>
// DOM Elements
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('file-input');
const fileInfo = document.getElementById('file-info');
const fileName = document.getElementById('file-name');
const fileSize = document.getElementById('file-size');
const transcribeBtn = document.getElementById('transcribe-btn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
const duration = document.getElementById('duration');
const processingTime = document.getElementById('processing-time');
const modelName = document.getElementById('model-name');
const transcriptionText = document.getElementById('transcription-text');
const downloadBtn = document.getElementById('download-btn');
const logContainer = document.getElementById('log-container');
const logContent = document.getElementById('log-content');
const errorMessage = document.getElementById('error-message');
// Server API URL
const API_URL = 'http://192.168.1.176:5042';
// State variables
let audioFile = null;
let transcriptionResult = null;
// Event listeners
dropzone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('dragover');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
handleFile(e.dataTransfer.files[0]);
}
});
transcribeBtn.addEventListener('click', transcribeAudio);
downloadBtn.addEventListener('click', downloadTranscription);
// Functions
function handleFileSelect(e) {
if (e.target.files.length) {
handleFile(e.target.files[0]);
}
}
function handleFile(file) {
// Check if it's an audio file
if (!file.type.startsWith('audio/')) {
showError('Пожалуйста, загрузите аудиофайл');
return;
}
audioFile = file;
// Update file info
fileName.textContent = file.name;
fileSize.textContent = formatFileSize(file.size);
fileInfo.style.display = 'block';
// Enable transcribe button
transcribeBtn.disabled = false;
// Reset previous results
result.style.display = 'none';
// Add log entry
addLogEntry(`Файл "${file.name}" выбран (${formatFileSize(file.size)})`);
// Show logs
logContainer.style.display = 'block';
}
async function transcribeAudio() {
if (!audioFile) return;
// Hide error and previous results
hideError();
result.style.display = 'none';
// Show loading
loading.style.display = 'block';
transcribeBtn.disabled = true;
// Create form data
const formData = new FormData();
formData.append('file', audioFile);
addLogEntry(`Началась загрузка файла "${audioFile.name}" на сервер`);
try {
const uploadStart = Date.now();
const response = await fetch(`${API_URL}/v1/audio/transcriptions`, {
method: 'POST',
body: formData
});
const uploadTime = ((Date.now() - uploadStart) / 1000).toFixed(1);
addLogEntry(`Файл успешно загружен и отправлен на обработку за ${uploadTime} сек.`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Сервер ответил с кодом ${response.status}`);
}
addLogEntry('Транскрибация успешно завершена');
transcriptionResult = await response.json();
// Log transcription details
addLogEntry(`Длительность аудио: ${formatDuration(transcriptionResult.duration_seconds)}`);
addLogEntry(`Время обработки: ${formatDuration(transcriptionResult.processing_time)}`);
addLogEntry(`Использованная модель: ${transcriptionResult.model}`);
// Display results
displayTranscriptionResults(transcriptionResult);
} catch (error) {
addLogEntry(`Ошибка: ${error.message}`, true);
showError(`Ошибка при транскрибации: ${error.message}`);
} finally {
loading.style.display = 'none';
transcribeBtn.disabled = false;
}
}
function displayTranscriptionResults(data) {
// Fill in the stats
duration.textContent = formatDuration(data.duration_seconds);
processingTime.textContent = `${data.processing_time.toFixed(1)} сек.`;
modelName.textContent = data.model;
// Display text
transcriptionText.textContent = data.text;
// Show result container
result.style.display = 'block';
}
function downloadTranscription() {
if (!transcriptionResult) return;
// Create JSON file
const jsonStr = JSON.stringify(transcriptionResult, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// Create text file with just the transcribed text
const textBlob = new Blob([transcriptionResult.text], { type: 'text/plain' });
const textUrl = URL.createObjectURL(textBlob);
// Create links and trigger downloads
const jsonLink = document.createElement('a');
jsonLink.href = url;
jsonLink.download = `transcription_${Date.now()}.json`;
document.body.appendChild(jsonLink);
jsonLink.click();
const textLink = document.createElement('a');
textLink.href = textUrl;
textLink.download = `transcription_${Date.now()}.txt`;
document.body.appendChild(textLink);
textLink.click();
// Clean up
setTimeout(() => {
document.body.removeChild(jsonLink);
document.body.removeChild(textLink);
URL.revokeObjectURL(url);
URL.revokeObjectURL(textUrl);
}, 100);
addLogEntry('Результаты транскрибации скачаны как JSON и TXT файлы');
}
function addLogEntry(message, isError = false) {
const entry = document.createElement('div');
entry.className = 'log-entry';
const time = document.createElement('span');
time.className = 'log-time';
time.textContent = new Date().toLocaleTimeString();
const text = document.createElement('span');
text.className = 'log-message';
if (isError) {
text.style.color = '#ef4444';
}
text.textContent = message;
entry.appendChild(time);
entry.appendChild(text);
logContent.appendChild(entry);
logContent.scrollTop = logContent.scrollHeight;
}
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
}
function hideError() {
errorMessage.style.display = 'none';
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatDuration(seconds) {
if (!seconds && seconds !== 0) return 'Неизвестно';
seconds = Math.round(seconds);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) {
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}:${remainingMinutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
// Initialize with a log entry
addLogEntry('Приложение готово к работе. Перетащите или выберите аудиофайл.');
logContainer.style.display = 'block';
</script>
</body>
</html>
+1
View File
@@ -2,6 +2,7 @@
"service_port": 5042,
"model_path": "/mnt/cloud/llm/whisper/whisper-large-v3-russian",
"language": "russian",
"enable_history": true,
"chunk_length_s": 30,
"batch_size": 16,
"max_new_tokens": 256,
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Имя окружения conda
CONDA_ENV_NAME="transcribe"
CONDA_ENV_NAME="whisper-api-server"
# Флаг обновления (по умолчанию false)
UPDATE_ENV=false