Initial clean commit

This commit is contained in:
Christian Krakau-Louis
2025-05-13 08:59:02 +00:00
commit 03b982e7c2
113 changed files with 22973 additions and 0 deletions
@@ -0,0 +1,461 @@
{% extends 'base.html' %}
{% block title %}Audio Settings - Quizzical Beats{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-navy-800 font-montserrat mb-2">Custom Audio Settings</h1>
<p class="text-gray-600">Upload or generate your own intro, outro, and replay announcements for quizzes.</p>
</div>
{% for category, message in get_flashed_messages(with_categories=true) %}
<div class="mb-6 p-4 rounded {% if category == 'danger' %}bg-red-50 text-red-700 border border-red-300{% elif category == 'success' %}bg-green-50 text-green-700 border border-green-300{% else %}bg-blue-50 text-blue-700 border border-blue-300{% endif %}">
{{ message }}
</div>
{% endfor %}
<!-- Intro Audio Section -->
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
<div class="bg-navy-50 p-4 border-b">
<h2 class="text-xl font-semibold text-navy-800">Intro Audio</h2>
<p class="text-gray-600 text-sm mt-1">This audio plays at the beginning of your music quiz</p>
</div>
<div class="p-6">
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-700 mb-2">Current Setting</h3>
{% if current_user.intro_mp3 %}
<div class="p-4 bg-navy-50 rounded-lg">
<p class="font-medium text-navy-800 mb-2">Custom intro audio is active</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/intro.mp3') if not current_user.intro_mp3 else url_for('core.serve_user_audio', filepath=current_user.intro_mp3) }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<form method="POST" class="mt-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="reset">
<input type="hidden" name="mp3_type" value="intro">
<button type="submit" class="text-red-600 text-sm hover:underline">Reset to default</button>
</form>
</div>
{% else %}
<div class="p-4 bg-gray-50 rounded-lg">
<p class="font-medium text-gray-700 mb-2">Using default intro audio</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/intro.mp3') }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
{% endif %}
</div>
<div class="border-t pt-6">
<h3 class="text-lg font-medium text-gray-700 mb-4">Customize</h3>
<div class="mb-8">
<h4 class="font-medium text-gray-700 mb-2">Option 1: Upload MP3</h4>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="upload">
<input type="hidden" name="mp3_type" value="intro">
<div class="flex items-center space-x-4">
<div class="flex-grow">
<input type="file" name="audio_file" accept=".mp3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500">
</div>
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 rounded transition-colors">
Upload
</button>
</div>
</form>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2">Option 2: Generate with Text-to-Speech</h4>
{% if has_tts_services %}
<form method="GET" class="mb-4 flex items-end space-x-2">
<input type="hidden" name="mp3_type" value="intro">
<label class="block text-sm font-medium text-gray-700 mb-1">TTS Service</label>
<select name="tts_service" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for svc in tts_services %}
<option value="{{ svc.id }}" {% if selected_service and svc.id == selected_service.id %}selected{% endif %}>{{ svc.name }}</option>
{% endfor %}
</select>
<button type="submit" class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-2 rounded">Change</button>
</form>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="generate">
<input type="hidden" name="mp3_type" value="intro">
<input type="hidden" name="tts_service" value="{{ selected_service.id }}">
<p class="mt-1 text-sm text-gray-500">{{ selected_service.description if selected_service else '' }}</p>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Voice</label>
<select name="tts_voice" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for voice in selected_service.voices %}
<option value="{{ voice.id }}">{{ voice.name }} ({{ voice.gender }}, {{ voice.language }})</option>
{% endfor %}
</select>
</div>
{% if selected_service.id == 'openai' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model Quality</label>
<select name="openai_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
<select name="elevenlabs_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.settings %}
<div class="mb-4 bg-gray-50 p-4 rounded-md">
<h5 class="font-medium text-gray-700 mb-2">Voice Settings</h5>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Stability <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="stability" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More variable</span>
<span>More stable</span>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Similarity Boost <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="similarity_boost" min="0" max="1" step="0.05" value="0.75" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More unique</span>
<span>More similar</span>
</div>
</div>
</div>
{% endif %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Text to Convert</label>
<textarea name="tts_text" rows="3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500"
placeholder="Enter text for intro announcement...">{{ default_texts.intro }}</textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded transition-colors">
Generate Audio
</button>
</div>
</form>
{% else %}
<div class="p-4 bg-yellow-50 text-yellow-700 border border-yellow-300 rounded-md">
<p>Text-to-speech generation requires API credentials (AWS Polly, OpenAI, or ElevenLabs). Contact your administrator to enable this feature.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Outro Audio Section -->
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
<div class="bg-navy-50 p-4 border-b">
<h2 class="text-xl font-semibold text-navy-800">Outro Audio</h2>
<p class="text-gray-600 text-sm mt-1">This audio plays at the end of your music quiz</p>
</div>
<div class="p-6">
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-700 mb-2">Current Setting</h3>
{% if current_user.outro_mp3 %}
<div class="p-4 bg-navy-50 rounded-lg">
<p class="font-medium text-navy-800 mb-2">Custom outro audio is active</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/outro.mp3') if not current_user.outro_mp3 else url_for('core.serve_user_audio', filepath=current_user.outro_mp3) }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<form method="POST" class="mt-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="reset">
<input type="hidden" name="mp3_type" value="outro">
<button type="submit" class="text-red-600 text-sm hover:underline">Reset to default</button>
</form>
</div>
{% else %}
<div class="p-4 bg-gray-50 rounded-lg">
<p class="font-medium text-gray-700 mb-2">Using default outro audio</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/outro.mp3') }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
{% endif %}
</div>
<div class="border-t pt-6">
<h3 class="text-lg font-medium text-gray-700 mb-4">Customize</h3>
<div class="mb-8">
<h4 class="font-medium text-gray-700 mb-2">Option 1: Upload MP3</h4>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="upload">
<input type="hidden" name="mp3_type" value="outro">
<div class="flex items-center space-x-4">
<div class="flex-grow">
<input type="file" name="audio_file" accept=".mp3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500">
</div>
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 rounded transition-colors">
Upload
</button>
</div>
</form>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2">Option 2: Generate with Text-to-Speech</h4>
{% if has_tts_services %}
<form method="GET" class="mb-4 flex items-end space-x-2">
<input type="hidden" name="mp3_type" value="outro">
<label class="block text-sm font-medium text-gray-700 mb-1">TTS Service</label>
<select name="tts_service" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for svc in tts_services %}
<option value="{{ svc.id }}" {% if selected_service and svc.id == selected_service.id %}selected{% endif %}>{{ svc.name }}</option>
{% endfor %}
</select>
<button type="submit" class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-2 rounded">Change</button>
</form>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="generate">
<input type="hidden" name="mp3_type" value="outro">
<input type="hidden" name="tts_service" value="{{ selected_service.id }}">
<p class="mt-1 text-sm text-gray-500">{{ selected_service.description if selected_service else '' }}</p>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Voice</label>
<select name="tts_voice" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for voice in selected_service.voices %}
<option value="{{ voice.id }}">{{ voice.name }} ({{ voice.gender }}, {{ voice.language }})</option>
{% endfor %}
</select>
</div>
{% if selected_service.id == 'openai' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model Quality</label>
<select name="openai_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
<select name="elevenlabs_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.settings %}
<div class="mb-4 bg-gray-50 p-4 rounded-md">
<h5 class="font-medium text-gray-700 mb-2">Voice Settings</h5>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Stability <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="stability" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More variable</span>
<span>More stable</span>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Similarity Boost <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="similarity_boost" min="0" max="1" step="0.05" value="0.75" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More unique</span>
<span>More similar</span>
</div>
</div>
</div>
{% endif %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Text to Convert</label>
<textarea name="tts_text" rows="3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500"
placeholder="Enter text for outro announcement...">{{ default_texts.outro }}</textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded transition-colors">
Generate Audio
</button>
</div>
</form>
{% else %}
<div class="p-4 bg-yellow-50 text-yellow-700 border border-yellow-300 rounded-md">
<p>Text-to-speech generation requires API credentials (AWS Polly, OpenAI, or ElevenLabs). Contact your administrator to enable this feature.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Replay Audio Section -->
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
<div class="bg-navy-50 p-4 border-b">
<h2 class="text-xl font-semibold text-navy-800">Replay Audio</h2>
<p class="text-gray-600 text-sm mt-1">This audio plays before replaying all songs at the end of the quiz</p>
</div>
<div class="p-6">
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-700 mb-2">Current Setting</h3>
{% if current_user.replay_mp3 %}
<div class="p-4 bg-navy-50 rounded-lg">
<p class="font-medium text-navy-800 mb-2">Custom replay audio is active</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/replay.mp3') if not current_user.replay_mp3 else url_for('core.serve_user_audio', filepath=current_user.replay_mp3) }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<form method="POST" class="mt-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="reset">
<input type="hidden" name="mp3_type" value="replay">
<button type="submit" class="text-red-600 text-sm hover:underline">Reset to default</button>
</form>
</div>
{% else %}
<div class="p-4 bg-gray-50 rounded-lg">
<p class="font-medium text-gray-700 mb-2">Using default replay audio</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/replay.mp3') }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
{% endif %}
</div>
<div class="border-t pt-6">
<h3 class="text-lg font-medium text-gray-700 mb-4">Customize</h3>
<div class="mb-8">
<h4 class="font-medium text-gray-700 mb-2">Option 1: Upload MP3</h4>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="upload">
<input type="hidden" name="mp3_type" value="replay">
<div class="flex items-center space-x-4">
<div class="flex-grow">
<input type="file" name="audio_file" accept=".mp3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500">
</div>
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 rounded transition-colors">
Upload
</button>
</div>
</form>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2">Option 2: Generate with Text-to-Speech</h4>
{% if has_tts_services %}
<form method="GET" class="mb-4 flex items-end space-x-2">
<input type="hidden" name="mp3_type" value="replay">
<label class="block text-sm font-medium text-gray-700 mb-1">TTS Service</label>
<select name="tts_service" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for svc in tts_services %}
<option value="{{ svc.id }}" {% if selected_service and svc.id == selected_service.id %}selected{% endif %}>{{ svc.name }}</option>
{% endfor %}
</select>
<button type="submit" class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-2 rounded">Change</button>
</form>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="generate">
<input type="hidden" name="mp3_type" value="replay">
<input type="hidden" name="tts_service" value="{{ selected_service.id }}">
<p class="mt-1 text-sm text-gray-500">{{ selected_service.description if selected_service else '' }}</p>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Voice</label>
<select name="tts_voice" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for voice in selected_service.voices %}
<option value="{{ voice.id }}">{{ voice.name }} ({{ voice.gender }}, {{ voice.language }})</option>
{% endfor %}
</select>
</div>
{% if selected_service.id == 'openai' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model Quality</label>
<select name="openai_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
<select name="elevenlabs_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.settings %}
<div class="mb-4 bg-gray-50 p-4 rounded-md">
<h5 class="font-medium text-gray-700 mb-2">Voice Settings</h5>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Stability <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="stability" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More variable</span>
<span>More stable</span>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Similarity Boost <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="similarity_boost" min="0" max="1" step="0.05" value="0.75" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More unique</span>
<span>More similar</span>
</div>
</div>
</div>
{% endif %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Text to Convert</label>
<textarea name="tts_text" rows="3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500"
placeholder="Enter text for replay announcement...">{{ default_texts.replay }}</textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded transition-colors">
Generate Audio
</button>
</div>
</form>
{% else %}
<div class="p-4 bg-yellow-50 text-yellow-700 border border-yellow-300 rounded-md">
<p>Text-to-speech generation requires API credentials (AWS Polly, OpenAI, or ElevenLabs). Contact your administrator to enable this feature.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="mt-8 text-center">
<a href="{{ url_for('users.profile') }}" class="inline-block bg-gray-500 hover:bg-gray-600 text-white py-2 px-4 rounded transition-colors">
Back to Profile
</a>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- No Alpine.js needed -->
{% endblock %}