mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
Add logic to provide tts to posts
This commit is contained in:
Binary file not shown.
@@ -76,6 +76,174 @@
|
||||
.think-icon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Enhanced Audio Player Styles */
|
||||
.audio-player {
|
||||
background: linear-gradient(145deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.audio-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.play-pause-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.play-pause-btn:hover {
|
||||
background: linear-gradient(145deg, #334155 0%, #1e293b 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.play-pause-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.play-pause-btn:disabled {
|
||||
background: #94a3b8;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stop-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(145deg, #dc2626 0%, #991b1b 100%);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 3px 6px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
.stop-btn:hover {
|
||||
background: linear-gradient(145deg, #ef4444 0%, #dc2626 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(220, 38, 38, 0.4);
|
||||
}
|
||||
|
||||
.stop-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 4px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
.speed-btn {
|
||||
width: 44px;
|
||||
height: 32px;
|
||||
background: linear-gradient(145deg, #059669 0%, #047857 100%);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(5, 150, 105, 0.3);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
}
|
||||
|
||||
.speed-btn:hover {
|
||||
background: linear-gradient(145deg, #10b981 0%, #059669 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 6px rgba(5, 150, 105, 0.4);
|
||||
}
|
||||
|
||||
.speed-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 4px rgba(5, 150, 105, 0.3);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #7c3aed 0%, #a855f7 100%);
|
||||
border-radius: 4px;
|
||||
transition: width 0.1s ease;
|
||||
box-shadow: 0 1px 3px rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
.seek-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.seek-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #7c3aed;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.audio-time {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
background: #f1f5f9;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.audio-status {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-ready { background: #ddd6fe; color: #7c3aed; }
|
||||
.status-generating { background: #fef3c7; color: #d97706; }
|
||||
.status-playing { background: #dcfce7; color: #16a34a; }
|
||||
.status-paused { background: #fed7aa; color: #ea580c; }
|
||||
.status-stopped { background: #f3f4f6; color: #6b7280; }
|
||||
.status-finished { background: #dbeafe; color: #2563eb; }
|
||||
.status-error { background: #fecaca; color: #dc2626; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -120,6 +288,14 @@
|
||||
|
||||
<!-- Action Buttons - Responsive positioning -->
|
||||
<div class="flex items-center space-x-2 sm:flex-shrink-0">
|
||||
<button onclick="toggleTTS()"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-purple-700 hover:text-purple-800 bg-purple-50 hover:bg-purple-100 rounded-md transition duration-150">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 14.142M9 9v6l4-3-4-3z"/>
|
||||
</svg>
|
||||
<span id="tts-button-text">{% trans "Listen" %}</span>
|
||||
</button>
|
||||
<a href="{% url 'post-update' post.pk %}"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-blue-700 hover:text-blue-800 bg-blue-50 hover:bg-blue-100 rounded-md transition duration-150">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -191,6 +367,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TTS Audio Player - Initially Hidden -->
|
||||
<div id="tts-player" class="mt-4 audio-player hidden">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="text-sm font-medium text-gray-800">{% trans "Audio Player" %}</h4>
|
||||
<div id="tts-status" class="audio-status status-ready">{% trans "Ready" %}</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio Controls -->
|
||||
<div class="space-y-4">
|
||||
<audio id="tts-audio" class="hidden" preload="none">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="progress-container">
|
||||
<div id="progress-bar" class="progress-bar" style="width: 0%"></div>
|
||||
<input id="seek-slider" type="range" min="0" max="100" value="0" class="seek-slider">
|
||||
</div>
|
||||
|
||||
<!-- Control Buttons and Time -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="audio-controls">
|
||||
<button id="play-pause-btn" onclick="togglePlayPause()"
|
||||
class="play-pause-btn"
|
||||
disabled>
|
||||
<svg id="play-icon" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||
<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>
|
||||
<svg id="pause-icon" class="w-6 h-6 hidden" fill="currentColor" viewBox="0 0 20 20">
|
||||
<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>
|
||||
|
||||
<button onclick="stopAudio()" class="stop-btn">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="speed-btn" onclick="toggleSpeed()" class="speed-btn" title="{% trans 'Playback Speed' %}">
|
||||
1x
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="audio-time">
|
||||
<span id="current-time">0:00</span> / <span id="total-time">0:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Section - Reduced padding, wider content -->
|
||||
@@ -242,5 +469,213 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TTS Functionality
|
||||
let currentAudio = null;
|
||||
let isLoading = false;
|
||||
let playbackSpeeds = [1, 1.25, 1.5, 2];
|
||||
let currentSpeedIndex = 0;
|
||||
|
||||
function toggleTTS() {
|
||||
const player = document.getElementById('tts-player');
|
||||
const buttonText = document.getElementById('tts-button-text');
|
||||
|
||||
if (player.classList.contains('hidden')) {
|
||||
player.classList.remove('hidden');
|
||||
buttonText.textContent = '{% trans "Hide Player" %}';
|
||||
|
||||
if (!currentAudio) {
|
||||
loadTTSAudio();
|
||||
}
|
||||
} else {
|
||||
player.classList.add('hidden');
|
||||
buttonText.textContent = '{% trans "Listen" %}';
|
||||
|
||||
if (currentAudio) {
|
||||
currentAudio.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadTTSAudio() {
|
||||
if (isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
const status = document.getElementById('tts-status');
|
||||
const playBtn = document.getElementById('play-pause-btn');
|
||||
|
||||
status.textContent = '{% trans "Generating..." %}';
|
||||
status.className = 'audio-status status-generating';
|
||||
playBtn.disabled = true;
|
||||
|
||||
fetch('{% url "post-tts" post.pk %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{ csrf_token }}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('TTS generation failed');
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
const audioUrl = URL.createObjectURL(blob);
|
||||
const audio = document.getElementById('tts-audio');
|
||||
|
||||
audio.src = audioUrl;
|
||||
currentAudio = audio;
|
||||
|
||||
audio.addEventListener('loadedmetadata', function() {
|
||||
status.textContent = '{% trans "Ready" %}';
|
||||
status.className = 'audio-status status-ready';
|
||||
playBtn.disabled = false;
|
||||
updateTotalTime();
|
||||
setupAudioEvents();
|
||||
});
|
||||
|
||||
audio.addEventListener('error', function() {
|
||||
status.textContent = '{% trans "Error" %}';
|
||||
status.className = 'audio-status status-error';
|
||||
playBtn.disabled = true;
|
||||
});
|
||||
|
||||
audio.load();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('TTS Error:', error);
|
||||
status.textContent = '{% trans "Failed" %}';
|
||||
status.className = 'audio-status status-error';
|
||||
playBtn.disabled = true;
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function setupAudioEvents() {
|
||||
if (!currentAudio) return;
|
||||
|
||||
currentAudio.addEventListener('timeupdate', updateProgress);
|
||||
currentAudio.addEventListener('ended', onAudioEnded);
|
||||
|
||||
// Set initial playback rate
|
||||
currentAudio.playbackRate = playbackSpeeds[currentSpeedIndex];
|
||||
|
||||
// Seek functionality
|
||||
const seekSlider = document.getElementById('seek-slider');
|
||||
seekSlider.addEventListener('input', function() {
|
||||
if (currentAudio) {
|
||||
const seekTime = (this.value / 100) * currentAudio.duration;
|
||||
currentAudio.currentTime = seekTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function togglePlayPause() {
|
||||
if (!currentAudio) return;
|
||||
|
||||
const playIcon = document.getElementById('play-icon');
|
||||
const pauseIcon = document.getElementById('pause-icon');
|
||||
const status = document.getElementById('tts-status');
|
||||
|
||||
if (currentAudio.paused) {
|
||||
currentAudio.play();
|
||||
playIcon.classList.add('hidden');
|
||||
pauseIcon.classList.remove('hidden');
|
||||
status.textContent = '{% trans "Playing" %}';
|
||||
status.className = 'audio-status status-playing';
|
||||
} else {
|
||||
currentAudio.pause();
|
||||
playIcon.classList.remove('hidden');
|
||||
pauseIcon.classList.add('hidden');
|
||||
status.textContent = '{% trans "Paused" %}';
|
||||
status.className = 'audio-status status-paused';
|
||||
}
|
||||
}
|
||||
|
||||
function stopAudio() {
|
||||
if (!currentAudio) return;
|
||||
|
||||
currentAudio.pause();
|
||||
currentAudio.currentTime = 0;
|
||||
|
||||
const playIcon = document.getElementById('play-icon');
|
||||
const pauseIcon = document.getElementById('pause-icon');
|
||||
const status = document.getElementById('tts-status');
|
||||
|
||||
playIcon.classList.remove('hidden');
|
||||
pauseIcon.classList.add('hidden');
|
||||
status.textContent = '{% trans "Stopped" %}';
|
||||
status.className = 'audio-status status-stopped';
|
||||
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
function toggleSpeed() {
|
||||
if (!currentAudio) return;
|
||||
|
||||
// Cycle through speeds
|
||||
currentSpeedIndex = (currentSpeedIndex + 1) % playbackSpeeds.length;
|
||||
const newSpeed = playbackSpeeds[currentSpeedIndex];
|
||||
|
||||
// Update audio playback rate
|
||||
currentAudio.playbackRate = newSpeed;
|
||||
|
||||
// Update button text
|
||||
const speedBtn = document.getElementById('speed-btn');
|
||||
speedBtn.textContent = newSpeed + 'x';
|
||||
|
||||
// Visual feedback
|
||||
speedBtn.style.transform = 'scale(1.1)';
|
||||
setTimeout(() => {
|
||||
speedBtn.style.transform = '';
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
if (!currentAudio) return;
|
||||
|
||||
const progress = (currentAudio.currentTime / currentAudio.duration) * 100;
|
||||
document.getElementById('progress-bar').style.width = progress + '%';
|
||||
document.getElementById('seek-slider').value = progress;
|
||||
|
||||
document.getElementById('current-time').textContent = formatTime(currentAudio.currentTime);
|
||||
}
|
||||
|
||||
function updateTotalTime() {
|
||||
if (!currentAudio) return;
|
||||
|
||||
document.getElementById('total-time').textContent = formatTime(currentAudio.duration);
|
||||
}
|
||||
|
||||
function onAudioEnded() {
|
||||
const playIcon = document.getElementById('play-icon');
|
||||
const pauseIcon = document.getElementById('pause-icon');
|
||||
const status = document.getElementById('tts-status');
|
||||
|
||||
playIcon.classList.remove('hidden');
|
||||
pauseIcon.classList.add('hidden');
|
||||
status.textContent = '{% trans "Finished" %}';
|
||||
status.className = 'audio-status status-finished';
|
||||
|
||||
// Reset speed to 1x when audio ends
|
||||
currentSpeedIndex = 0;
|
||||
const speedBtn = document.getElementById('speed-btn');
|
||||
speedBtn.textContent = '1x';
|
||||
if (currentAudio) {
|
||||
currentAudio.playbackRate = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (isNaN(seconds)) return '0:00';
|
||||
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return mins + ':' + (secs < 10 ? '0' : '') + secs;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -41,6 +41,7 @@ urlpatterns = [
|
||||
path('ui/posts/<int:pk>/', post_views.PostDetailView.as_view(), name='post-detail'),
|
||||
path('ui/posts/<int:pk>/edit/', post_views.PostUpdateView.as_view(), name='post-update'),
|
||||
path('ui/posts/<int:pk>/delete/', post_views.PostDeleteView.as_view(), name='post-delete'),
|
||||
path('ui/posts/<int:post_id>/tts/', views.generate_tts, name='post-tts'),
|
||||
|
||||
# Collection slideshow
|
||||
path('ui/collections/<uuid:pk>/slideshow/',
|
||||
|
||||
+63
-5
@@ -29,12 +29,10 @@ from wsgiref.util import FileWrapper
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
import requests
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from .serializers import PageSerializer
|
||||
|
||||
from .page_views import (
|
||||
PageListView, PageDetailView, PageCreateView,
|
||||
PageUpdateView, PageDeleteView, fetch_page_info,
|
||||
@@ -510,3 +508,63 @@ def export_database(request):
|
||||
|
||||
class HelpView(TemplateView):
|
||||
template_name = 'links/help.html'
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def generate_tts(request, post_id):
|
||||
"""Generate TTS audio for a post"""
|
||||
try:
|
||||
post = get_object_or_404(Post, pk=post_id)
|
||||
|
||||
# Clean the post content - remove markdown and HTML
|
||||
clean_text = clean_text_for_tts(post.content)
|
||||
|
||||
if not clean_text.strip():
|
||||
return JsonResponse({'error': 'No text content to convert'}, status=400)
|
||||
|
||||
# Call TTS API
|
||||
import urllib.parse
|
||||
encoded_text = urllib.parse.quote(clean_text)
|
||||
tts_url = f"https://home-tts.junv.workers.dev/tts?text={encoded_text}&haha=hahaJunv"
|
||||
|
||||
try:
|
||||
response = requests.get(tts_url, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
# Return the audio file
|
||||
audio_response = HttpResponse(response.content, content_type='audio/mpeg')
|
||||
audio_response['Content-Disposition'] = f'inline; filename="post_{post_id}_tts.mp3"'
|
||||
return audio_response
|
||||
|
||||
except requests.RequestException as e:
|
||||
return JsonResponse({'error': f'TTS service error: {str(e)}'}, status=500)
|
||||
|
||||
except Post.DoesNotExist:
|
||||
return JsonResponse({'error': 'Post not found'}, status=404)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': f'Server error: {str(e)}'}, status=500)
|
||||
|
||||
def clean_text_for_tts(content):
|
||||
"""Clean markdown and HTML from text for TTS"""
|
||||
# Convert markdown to HTML first
|
||||
html = markdown.markdown(content)
|
||||
|
||||
# Parse HTML and extract text
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
|
||||
# Remove script and style elements
|
||||
for script in soup(["script", "style"]):
|
||||
script.decompose()
|
||||
|
||||
# Get text and clean it
|
||||
text = soup.get_text()
|
||||
|
||||
# Clean up whitespace and special characters
|
||||
text = re.sub(r'\s+', ' ', text) # Multiple spaces to single
|
||||
text = re.sub(r'\n+', '. ', text) # Multiple newlines to period
|
||||
text = text.strip()
|
||||
|
||||
# Limit text length (TTS services often have limits)
|
||||
if len(text) > 3000:
|
||||
text = text[:3000] + "..."
|
||||
|
||||
return text
|
||||
|
||||
@@ -244,16 +244,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.0.11"
|
||||
version = "5.0.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/9c/614ef004c122cfdf9f2d291eab75e24ab94d24c0693a0ca0db44e9633dc3/Django-5.0.11.tar.gz", hash = "sha256:e7d98fa05ce09cb3e8d5ad6472fb602322acd1740bfdadc29c8404182d664f65", size = 10635979, upload-time = "2025-01-14T14:28:58.251Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/cc0205045386b5be8eecb15a95f290383d103f0db5f7e34f93dcc340d5b0/Django-5.0.14.tar.gz", hash = "sha256:29019a5763dbd48da1720d687c3522ef40d1c61be6fb2fad27ed79e9f655bc11", size = 10644306, upload-time = "2025-04-02T11:24:41.396Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/d7/da8d8f62f33c3cd45ad7e9cb456d7c95457d15ee26166d2d033f461aedf7/Django-5.0.11-py3-none-any.whl", hash = "sha256:09e8128f717266bf382d82ffa4933f13da05d82579abf008ede86acb15dec88b", size = 8185849, upload-time = "2025-01-14T14:28:46.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/93/eabde8789f41910845567ebbff5aacd52fd80e54c934ce15b83d5f552d2c/Django-5.0.14-py3-none-any.whl", hash = "sha256:e762bef8629ee704de215ebbd32062b84f4e56327eed412e5544f6f6eb1dfd74", size = 8185934, upload-time = "2025-04-02T11:24:36.888Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -338,14 +338,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "22.0.0"
|
||||
version = "23.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/88/e2f93c5738a4c1f56a458fc7a5b1676fc31dcdbb182bef6b40a141c17d66/gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63", size = 3639760, upload-time = "2024-04-16T22:58:19.218Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/29/97/6d610ae77b5633d24b69c2ff1ac3044e0e565ecbd1ec188f02c45073054c/gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", size = 84443, upload-time = "2024-04-16T22:58:15.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -863,7 +863,7 @@ requires-dist = [
|
||||
{ name = "black", marker = "extra == 'dev'", specifier = ">=23.0" },
|
||||
{ name = "boto3", specifier = ">=1.35.0" },
|
||||
{ name = "celery", specifier = ">=5.3.0" },
|
||||
{ name = "django", specifier = "==5.0.11" },
|
||||
{ name = "django", specifier = "==5.0.14" },
|
||||
{ name = "django-simplemde", specifier = "==0.1.4" },
|
||||
{ name = "django-tailwind", specifier = "==3.8.0" },
|
||||
{ name = "django-widget-tweaks", specifier = "==1.5.0" },
|
||||
@@ -871,7 +871,7 @@ requires-dist = [
|
||||
{ name = "flake8", marker = "extra == 'dev'", specifier = ">=6.0" },
|
||||
{ name = "flower", specifier = ">=2.0.0" },
|
||||
{ name = "fontawesome-free", specifier = "==5.15.3" },
|
||||
{ name = "gunicorn", specifier = "==22.0.0" },
|
||||
{ name = "gunicorn", specifier = "==23.0.0" },
|
||||
{ name = "isort", marker = "extra == 'dev'", specifier = ">=5.0" },
|
||||
{ name = "markdown", specifier = "==3.7" },
|
||||
{ name = "pillow", specifier = "~=11.2.1" },
|
||||
|
||||
Reference in New Issue
Block a user