nice audio player

This commit is contained in:
lelo 2025-03-19 17:47:01 +01:00
parent ab79098e9a
commit b7c4360560
9 changed files with 346 additions and 44 deletions

View File

@ -0,0 +1,6 @@
{
"dev_key_f83745ft0g5rg3": {
"expiry" : "31.12.2000",
"file_root": "\\\\path\\if\\using\\windows"
}
}

17
app.py
View File

@ -18,6 +18,7 @@ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
app.config['SECRET_KEY'] = '85c1117eb3a5f2c79f0ff395bada8ff8d9a257b99ef5e143'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=90)
if os.environ.get('FLASK_ENV') == 'production':
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
app.config['SESSION_COOKIE_SECURE'] = True
@ -29,20 +30,23 @@ def load_allowed_secrets(filename='allowed_secrets.json'):
value['expiry'] = datetime.strptime(value['expiry'], '%d.%m.%Y').date()
return secrets
app.config['ALLOWED_SECRETS'] = load_allowed_secrets()
def require_secret(f):
@wraps(f)
def decorated_function(*args, **kwargs):
allowed_secrets = app.config['ALLOWED_SECRETS']
allowed_secrets = load_allowed_secrets()
today = date.today()
def is_valid(secret_data):
print("is_valid:", secret_data)
expiry_date = secret_data.get('expiry')
return expiry_date and today <= expiry_date
is_valid = expiry_date and today <= expiry_date
print("is_valid", is_valid)
return is_valid
# Check if a secret was provided via GET parameter
get_secret = request.args.get('secret')
print("request.args:", request.args)
print("get_secret:", get_secret)
if get_secret is not None:
secret_data = allowed_secrets.get(get_secret)
if secret_data:
@ -51,13 +55,17 @@ def require_secret(f):
session['secret'] = get_secret
session.permanent = True
app.config['FILE_ROOT'] = secret_data.get('file_root')
print("session:", session['secret'])
return f(*args, **kwargs)
else:
# Secret provided via URL is expired or invalid
return render_template('error.html', message="Invalid or expired secret."), 403
# If no secret provided via GET, check the session
session_secret = session.get('secret')
print("session_secret", session_secret)
if session_secret is not None:
secret_data = allowed_secrets.get(session_secret)
if secret_data:
@ -186,6 +194,7 @@ def generate_breadcrumbs(subpath):
@app.route('/api/path/<path:subpath>')
@require_secret
def api_browse(subpath):
print("subpath:", subpath)
file_root = app.config['FILE_ROOT']
directory = os.path.join(file_root, subpath.replace('/', os.sep))

View File

@ -0,0 +1,5 @@
openai-whisper
#https://pytorch.org/get-started/locally/
torch==2.5.1
torchvision==0.20.1
torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu124

View File

@ -91,7 +91,7 @@ function loadDirectory(subpath) {
})
.catch(error => {
console.error('Error loading directory:', error);
document.getElementById('content').innerHTML = '<p>Error loading directory.</p>';
document.getElementById('content').innerHTML = '<p>Error loading directory!</p><p>'+error+'</p>';
});
}
@ -143,6 +143,7 @@ document.querySelectorAll('.play-file').forEach(link => {
const fileType = this.getAttribute('data-file-type');
const relUrl = this.getAttribute('data-url');
const nowPlayingInfo = document.getElementById('nowPlayingInfo');
document.getElementById('audioPlayerContainer').style.display = "block"
// Remove the class from all file items.
document.querySelectorAll('.file-item').forEach(item => {
@ -154,6 +155,7 @@ document.querySelectorAll('.play-file').forEach(link => {
const idx = this.getAttribute('data-index');
const audioPlayer = document.getElementById('globalAudio');
currentMusicIndex = idx !== null ? parseInt(idx) : -1;
const playerButton = document.querySelector('.player-button');
// Add the class to the clicked file item's parent.
this.closest('.file-item').classList.add('currently-playing');
@ -167,7 +169,11 @@ document.querySelectorAll('.play-file').forEach(link => {
audioPlayer.pause();
audioPlayer.src = ''; // Clear existing source.
// only show this if it takes longer. avoid flicker
let loaderTimeout = setTimeout(() => {
playerButton.innerHTML = playIcon;
nowPlayingInfo.textContent = "Wird geladen...";
}, 500);
document.querySelector('footer').style.display = 'flex';
const mediaUrl = '/media/' + relUrl;
@ -175,7 +181,7 @@ document.querySelectorAll('.play-file').forEach(link => {
try {
// Pass the signal to the fetch so it can be aborted.
const response = await fetch(mediaUrl, { method: 'HEAD', signal: currentFetchController.signal });
clearTimeout(loaderTimeout); // done loading. stop timeout
if (response.status === 403) {
nowPlayingInfo.textContent = "Fehler: Zugriff verweigert.";
window.location.href = '/'; // Redirect if forbidden.
@ -186,6 +192,7 @@ document.querySelectorAll('.play-file').forEach(link => {
audioPlayer.src = mediaUrl;
audioPlayer.load();
audioPlayer.play();
playerButton.innerHTML = pauseIcon;
const pathParts = relUrl.split('/');
const fileName = pathParts.pop();
const pathStr = pathParts.join('/');

121
static/audioplayer.css Normal file
View File

@ -0,0 +1,121 @@
.audio-player-container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
padding: 7px;
width: 100%;
display: none;
}
/* Now Playing Info */
.now-playing-info {
margin-top: 5px;
font-size: 16px;
color: #23313f;
text-align: center;
}
.audio-player {
--player-button-width: 4em;
--sound-button-width: 3em;
--space: 0.5em;
}
.controls {
display: flex;
align-items: center;
width: 100%;
gap: var(--space); /* ensures spacing between controls */
}
/* Make the slider container fill the available space and stack its children vertically */
.slider {
flex: 1;
display: flex;
flex-direction: column;
}
/* The range input takes the full width of its container */
.timeline {
-webkit-appearance: none;
width: 100%;
height: 0.5em;
background-color: #ccc; /* light gray background */
border-radius: 5px;
background-size: 0% 100%;
background-image: linear-gradient(#34495e, #34495e); /* progress bar */
background-repeat: no-repeat;
appearance: none;
outline: none;
}
/* Slider Thumb Styling */
/* WebKit browsers (Chrome, Safari) */
.timeline::-webkit-slider-thumb {
-webkit-appearance: none;
width: 1em;
height: 1em;
border-radius: 50%;
background-color: #34495e;
cursor: pointer;
border: none;
outline: none;
}
/* Firefox */
.timeline::-moz-range-thumb {
width: 1em;
height: 1em;
border-radius: 50%;
background-color: #34495e;
cursor: pointer;
border: none;
outline: none;
}
/* Internet Explorer and Edge */
.timeline::-ms-thumb {
width: 1em;
height: 1em;
border-radius: 50%;
background-color: #34495e;
cursor: pointer;
border: none;
outline: none;
}
/* Remove default track styling */
.timeline::-webkit-slider-runnable-track,
.timeline::-moz-range-track,
.timeline::-ms-track {
-webkit-appearance: none;
box-shadow: none;
border: none;
background: transparent;
}
/* Style the time info (positioned right below the slider) */
.now-playing-info {
text-align: center;
font-size: 0.8em;
margin-top: 0.25em;
}
.player-button,
.sound-button {
background-color: transparent;
border: 0;
cursor: pointer;
padding: 0;
}
.player-button {
width: var(--player-button-width);
height: var(--player-button-width);
}
.sound-button {
width: var(--sound-button-width);
height: var(--sound-button-width);
}

157
static/audioplayer.js Normal file
View File

@ -0,0 +1,157 @@
const playerButton = document.querySelector('.player-button'),
audio = document.querySelector('audio'),
timeline = document.querySelector('.timeline'),
soundButton = document.querySelector('.sound-button'),
timeInfo = document.getElementById('timeInfo'),
playIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
</svg>
`,
pauseIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
`,
soundIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clip-rule="evenodd" />
</svg>
`,
muteIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM12.293 7.293a1 1 0 011.414 0L15 8.586l1.293-1.293a1 1 0 111.414 1.414L16.414 10l1.293 1.293a1 1 0 01-1.414 1.414L15 11.414l-1.293 1.293a1 1 0 01-1.414-1.414L13.586 10l-1.293-1.293a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
`;
function toggleAudio () {
if (audio.paused) {
audio.play();
playerButton.innerHTML = pauseIcon;
} else {
audio.pause();
playerButton.innerHTML = playIcon;
}
}
playerButton.addEventListener('click', toggleAudio);
let isSeeking = false;
// --- Slider (Timeline) events ---
// Mouse events
timeline.addEventListener('mousedown', () => { isSeeking = true; });
timeline.addEventListener('mouseup', () => {
isSeeking = false;
changeSeek(); // Update the audio currentTime based on the slider position
});
timeline.addEventListener('change', changeSeek);
// Touch events
timeline.addEventListener('touchstart', () => { isSeeking = true; });
timeline.addEventListener('touchend', () => {
isSeeking = false;
changeSeek();
});
timeline.addEventListener('touchcancel', () => { isSeeking = false; });
// --- When metadata is loaded, set slider range in seconds ---
audio.addEventListener('loadedmetadata', () => {
updateTimeInfo();
timeline.min = 0;
timeline.max = audio.duration;
timeline.value = 0; // Ensures the thumb starts at the left.
});
// --- Update the slider thumb position (in seconds) while playing ---
function changeTimelinePosition() {
if (!isSeeking) {
timeline.value = audio.currentTime;
const percentagePosition = (audio.currentTime / audio.duration) * 100;
timeline.style.backgroundSize = `${percentagePosition}% 100%`;
}
}
// --- Seek function: directly set audio.currentTime using slider's value (in seconds) ---
function changeSeek() {
audio.currentTime = timeline.value;
}
// --- Sound toggle ---
function toggleSound () {
audio.muted = !audio.muted;
soundButton.innerHTML = audio.muted ? muteIcon : soundIcon;
}
soundButton.addEventListener('click', toggleSound);
// --- Utility: Format seconds as mm:ss ---
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`;
}
// --- Update the time display ---
function updateTimeInfo() {
const currentTimeFormatted = formatTime(audio.currentTime);
const durationFormatted = isNaN(audio.duration) ? "00:00" : formatTime(audio.duration);
timeInfo.innerHTML = `${currentTimeFormatted} / ${durationFormatted}`;
}
// --- Update timeline, time info, and media session on each time update ---
audio.ontimeupdate = function() {
changeTimelinePosition();
updateTimeInfo();
if ('mediaSession' in navigator && typeof navigator.mediaSession.setPositionState === 'function') {
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: audio.currentTime
});
}
};
// --- Also update media session on play (in case timeupdate lags) ---
audio.onplay = function() {
if ('mediaSession' in navigator && typeof navigator.mediaSession.setPositionState === 'function') {
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: audio.currentTime
});
}
};
// --- Handle external seek requests (e.g. from Android or Windows system controls) ---
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('seekto', function(details) {
if (details.fastSeek && 'fastSeek' in audio) {
audio.fastSeek(details.seekTime);
} else {
audio.currentTime = details.seekTime;
}
changeTimelinePosition();
});
}
// --- When audio ends ---
audio.onended = function() {
playerButton.innerHTML = playIcon;
};
function downloadAudio() {
// Get the current audio source URL
const audioSrc = audio.currentSrc || audio.src;
if (audioSrc) {
// Create a temporary link element
const a = document.createElement('a');
a.href = audioSrc;
// Extract the file name from the URL, decode it, and set as download attribute
let fileName = audioSrc.split('/').pop() || 'audio';
fileName = decodeURIComponent(fileName);
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}

View File

@ -127,34 +127,6 @@ footer {
align-items: center;
justify-content: center;
}
footer audio {
width: 75%;
transform: scale(1.3); /* Default for other devices */
padding: 10px 0;
}
/* Target Safari specifically */
@supports (-webkit-touch-callout: none) {
footer audio {
width: 60%;
transform: scale(1.6); /* Different scale for Apple devices */
}
}
.audio-player-container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
padding: 7px;
width: 100%;
}
/* Now Playing Info */
.now-playing-info {
margin-top: 5px;
font-size: 16px;
color: #2c3e50;
text-align: center;
}
/* Modal Styles */
#transcriptModal {

View File

@ -28,6 +28,7 @@
<!-- Your CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='gallery.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='audioplayer.css') }}">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
/* Header styles */
@ -62,12 +63,35 @@
<div id="content"></div>
</div>
<!-- Global Audio Player in Footer -->
<footer style="display: none;">
<div class="audio-player-container">
<audio id="globalAudio" controls preload="auto">
<!-- Global Audio Player in Footer style="display: none;"-->
<footer >
<div class="audio-player-container" id="audioPlayerContainer">
<div class="audio-player">
<audio id="globalAudio">
Your browser does not support the audio element.
</audio>
<div class="controls">
<button class="player-button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
<div class="slider">
<input type="range" class="timeline" max="100" value="0" step="0.1">
<div id="timeInfo" class="now-playing-info"></div>
</div>
<button class="sound-button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clip-rule="evenodd" />
</svg>
</button>
<button class="sound-button" onclick="downloadAudio()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 120.89" fill="#34495e" width="30" height="30">
<path fill-rule="evenodd" d="M84.58,47a7.71,7.71,0,1,1,10.8,11L66.09,86.88a7.72,7.72,0,0,1-10.82,0L26.4,58.37a7.71,7.71,0,1,1,10.81-11L53.1,63.12l.16-55.47a7.72,7.72,0,0,1,15.43.13l-.15,55L84.58,47ZM0,113.48.1,83.3a7.72,7.72,0,1,1,15.43.14l-.07,22q46,.09,91.91,0l.07-22.12a7.72,7.72,0,1,1,15.44.14l-.1,30h-.09a7.71,7.71,0,0,1-7.64,7.36q-53.73.1-107.38,0A7.7,7.7,0,0,1,0,113.48Z"/>
</svg>
</button>
</div>
</div>
<div id="nowPlayingInfo" class="now-playing-info"></div>
</div>
</footer>
@ -93,6 +117,7 @@
<!-- Load main app JS first, then gallery JS -->
<script src="{{ url_for('static', filename='app.js') }}"></script>
<script src="{{ url_for('static', filename='gallery.js') }}"></script>
<script src="{{ url_for('static', filename='audioplayer.js') }}"></script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {

View File

@ -212,7 +212,7 @@ def process_folder(root_folder):
print(f"Checked {checked_files} files. Start to transcribe {len(valid_files)} files.")
print("Loading Whisper model...")
model = whisper.load_model(model_name)
model = whisper.load_model(model_name, device="cuda")
# Use a thread pool to pre-load files concurrently.
with concurrent.futures.ThreadPoolExecutor() as executor: