This commit is contained in:
Mann Patel
2026-02-15 11:21:29 -07:00
parent b9282a44c8
commit 1c60fae113
8 changed files with 221 additions and 201 deletions

View File

@@ -445,6 +445,10 @@ def api_get_post(post_id: int):
row = get_audio_post_by_id(post_id)
if not row:
return _error("Post not found.", 404)
# CRITICAL: Add audio URL to the response
row = _add_audio_url(row)
return jsonify(row)

View File

@@ -150,7 +150,7 @@ class ApiClient {
}
async getAudioUrl(postId, expiresIn = 3600) {
return this.request(`/posts/${postId}/audio?expires_in=${expiresIn}`);
return this.request(`/posts/${postId}/audio-url`);
}
// ==================== Post Files ====================

View File

@@ -0,0 +1,139 @@
import { Play, Pause, Volume2 } from 'lucide-react'
import { useState, useRef, useEffect } from 'react'
export default function AudioPlayer({ src, onEnded, className = '' }) {
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [volume, setVolume] = useState(1)
const audioRef = useRef(null)
useEffect(() => {
if (audioRef.current && src) {
audioRef.current.src = src
audioRef.current.load()
}
}, [src])
const formatTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
const togglePlay = () => {
if (!audioRef.current || !src) return
if (isPlaying) {
audioRef.current.pause()
} else {
audioRef.current.play()
}
setIsPlaying(!isPlaying)
}
const handleTimeUpdate = () => {
if (audioRef.current) {
setCurrentTime(audioRef.current.currentTime)
}
}
const handleLoadedMetadata = () => {
if (audioRef.current) {
setDuration(audioRef.current.duration)
}
}
const handleSeek = (e) => {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const percentage = x / rect.width
const newTime = percentage * duration
if (audioRef.current) {
audioRef.current.currentTime = newTime
setCurrentTime(newTime)
}
}
const handleVolumeChange = (e) => {
const newVolume = parseFloat(e.target.value)
setVolume(newVolume)
if (audioRef.current) {
audioRef.current.volume = newVolume
}
}
const handleEnded = () => {
setIsPlaying(false)
setCurrentTime(0)
if (onEnded) onEnded()
}
const handleAudioError = (e) => {
console.error('Audio error:', e)
console.error('Audio element error:', audioRef.current?.error)
}
const progress = duration > 0 ? (currentTime / duration) * 100 : 0
if (!src) return null
return (
<div className={className}>
<audio
ref={audioRef}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onEnded={handleEnded}
onError={handleAudioError}
preload="metadata"
crossOrigin="anonymous"
/>
<div className="flex items-center gap-4">
<button
onClick={togglePlay}
className="w-10 h-10 bg-[#f4b840] hover:bg-[#e5a930] rounded-full flex items-center justify-center text-[#1a1a1a] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isPlaying ? (
<Pause size={16} fill="currentColor" />
) : (
<Play size={16} fill="currentColor" />
)}
</button>
<div className="flex-1">
<div
className="h-1.5 bg-gray-300 rounded-full overflow-hidden mb-2 cursor-pointer"
onClick={handleSeek}
>
<div
className="h-full bg-[#f4b840] rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<div className="flex justify-between text-xs text-gray-600">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
<div className="flex items-center gap-2">
<Volume2 size={18} className="text-gray-600" />
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={handleVolumeChange}
className="w-16 h-1.5 bg-gray-300 rounded-full appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #f4b840 0%, #f4b840 ${volume * 100}%, #d1d5db ${volume * 100}%, #d1d5db 100%)`
}}
/>
</div>
</div>
</div>
)
}

View File

@@ -1,19 +1,13 @@
import { Play, Pause, Volume2, Clock, ChevronDown, ChevronUp, Download, ExternalLink } from 'lucide-react'
import { useState, useRef, useEffect } from 'react'
import { Clock, Download, ExternalLink } from 'lucide-react'
import { useState, useEffect } from 'react'
import { api } from '../api'
import AudioPlayer from './AudioPlayer'
export default function AudioPostCard({ post, onViewPost }) {
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [volume, setVolume] = useState(1)
const [transcript, setTranscript] = useState(null)
const [loadingTranscript, setLoadingTranscript] = useState(false)
const [transcriptExpanded, setTranscriptExpanded] = useState(false)
const [downloading, setDownloading] = useState(false)
const audioRef = useRef(null)
// DEBUG: Log post data to console
useEffect(() => {
console.log('Post data:', post)
console.log('Audio URL:', post.audio_url)
@@ -29,25 +23,11 @@ export default function AudioPostCard({ post, onViewPost }) {
minute: '2-digit'
})
}
const formatTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
// Load transcript on mount if post is ready
useEffect(() => {
if (post.status === 'ready' && !transcript && !loadingTranscript) {
loadTranscript()
}
// Set audio source if available
if (post.audio_url && audioRef.current) {
console.log('Setting audio src to:', post.audio_url)
audioRef.current.src = post.audio_url
audioRef.current.load() // Force reload
}
}, [post.post_id, post.status, post.audio_url])
const loadTranscript = async () => {
@@ -95,69 +75,12 @@ export default function AudioPostCard({ post, onViewPost }) {
}
}
const togglePlay = () => {
if (!audioRef.current) return
if (isPlaying) {
audioRef.current.pause()
} else {
audioRef.current.play()
}
setIsPlaying(!isPlaying)
}
const handleTimeUpdate = () => {
if (audioRef.current) {
setCurrentTime(audioRef.current.currentTime)
}
}
const handleLoadedMetadata = () => {
if (audioRef.current) {
console.log('Audio metadata loaded, duration:', audioRef.current.duration)
setDuration(audioRef.current.duration)
}
}
const handleAudioError = (e) => {
console.error('Audio error:', e)
console.error('Audio element error:', audioRef.current?.error)
}
const handleSeek = (e) => {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const percentage = x / rect.width
const newTime = percentage * duration
if (audioRef.current) {
audioRef.current.currentTime = newTime
setCurrentTime(newTime)
}
}
const handleVolumeChange = (e) => {
const newVolume = parseFloat(e.target.value)
setVolume(newVolume)
if (audioRef.current) {
audioRef.current.volume = newVolume
}
}
const handleEnded = () => {
setIsPlaying(false)
setCurrentTime(0)
}
const handleView = (postId) => {
if (onViewPost) {
onViewPost(postId)
}
}
const progress = duration > 0 ? (currentTime / duration) * 100 : 0
return (
<article className="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm hover:shadow-md transition-shadow">
{/* Post Header */}
@@ -239,60 +162,7 @@ export default function AudioPostCard({ post, onViewPost }) {
{post.status === 'ready' && (
<>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
{/* Hidden audio element */}
<audio
ref={audioRef}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onEnded={handleEnded}
onError={handleAudioError}
onCanPlay={() => console.log('Audio can play')}
preload="metadata"
crossOrigin="anonymous"
/>
<div className="flex items-center gap-4">
<button
onClick={togglePlay}
className="w-10 h-10 bg-[#f4b840] hover:bg-[#e5a930] rounded-full flex items-center justify-center text-[#1a1a1a] transition-colors"
>
{isPlaying ? (
<Pause size={16} fill="currentColor" />
) : (
<Play size={16} fill="currentColor" />
)}
</button>
<div className="flex-1">
<div
className="h-1.5 bg-gray-300 rounded-full overflow-hidden mb-2 cursor-pointer"
onClick={handleSeek}
>
<div
className="h-full bg-[#f4b840] rounded-full transition-all"
style={{ width: `${progress}%` }}
></div>
</div>
<div className="flex justify-between text-xs text-gray-600">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
<div className="flex items-center gap-2">
<Volume2 size={18} className="text-gray-600" />
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={handleVolumeChange}
className="w-16 h-1.5 bg-gray-300 rounded-full appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #f4b840 0%, #f4b840 ${volume * 100}%, #d1d5db ${volume * 100}%, #d1d5db 100%)`
}}
/>
</div>
</div>
<AudioPlayer src={post.audio_url} />
</div>
{/* Transcript Section - Always shown */}

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'
import { ArrowLeft, Play, Pause, Volume2, Clock, Download, Share2, Calendar, Globe, Lock } from 'lucide-react'
import { ArrowLeft, Clock, Download, Share2, Calendar, Globe, Lock, Play, Pause, Volume2 } from 'lucide-react'
import { api } from '../api'
export default function PostDetail({ postId, user, onBack }) {
@@ -8,13 +8,13 @@ export default function PostDetail({ postId, user, onBack }) {
const [chunks, setChunks] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [downloading, setDownloading] = useState(false)
// Audio player state
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [volume, setVolume] = useState(1)
const [downloading, setDownloading] = useState(false)
const audioRef = useRef(null)
useEffect(() => {
@@ -23,31 +23,25 @@ export default function PostDetail({ postId, user, onBack }) {
}
}, [postId])
// Add this useEffect in PostDetail
// Set audio source when post loads
useEffect(() => {
if (audioRef.current && post?.audio_url) {
console.log('Setting audio src:', post.audio_url)
if (post?.audio_url && audioRef.current) {
console.log('Setting audio source:', post.audio_url)
audioRef.current.src = post.audio_url
audioRef.current.load() // Force reload
audioRef.current.load()
}
}, [post?.audio_url])
const loadPostData = async () => {
setLoading(true)
setError(null)
try {
// Load post data
// Load post data (should already include audio_url from backend)
const postData = await api.getPost(postId)
console.log('Loaded post data:', postData)
setPost(postData)
// Load audio URL if available
if (postData.audio_url && audioRef.current) {
audioRef.current.src = postData.audio_url
audioRef.current.load()
}
// Load metadata (contains transcript)
try {
const metadataResponse = await api.getPostMetadata(postId)
@@ -99,7 +93,8 @@ useEffect(() => {
// Audio player handlers
const togglePlay = () => {
if (!audioRef.current) return
if (!audioRef.current || !post?.audio_url) return
if (isPlaying) {
audioRef.current.pause()
} else {
@@ -116,6 +111,7 @@ useEffect(() => {
const handleLoadedMetadata = () => {
if (audioRef.current) {
console.log('Audio metadata loaded, duration:', audioRef.current.duration)
setDuration(audioRef.current.duration)
}
}
@@ -145,6 +141,11 @@ useEffect(() => {
setCurrentTime(0)
}
const handleAudioError = (e) => {
console.error('Audio error:', e)
console.error('Audio element error:', audioRef.current?.error)
}
const handleDownload = async () => {
if (downloading) return
@@ -175,7 +176,6 @@ useEffect(() => {
url: window.location.href
})
} else {
// Copy link to clipboard
navigator.clipboard.writeText(window.location.href)
alert('Link copied to clipboard!')
}
@@ -294,7 +294,7 @@ useEffect(() => {
</div>
{/* Audio Player */}
{post.status === 'ready' && (
{post.status === 'ready' && post.audio_url && (
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Audio Player</h2>
@@ -303,10 +303,14 @@ useEffect(() => {
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onEnded={handleEnded}
onError={handleAudioError}
onCanPlay={() => console.log('Audio can play')}
preload="metadata"
crossOrigin="anonymous"
/>
<div className="flex items-center gap-4 mb-4">
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="flex items-center gap-4">
<button
onClick={togglePlay}
className="w-12 h-12 bg-[#f4b840] hover:bg-[#e5a930] rounded-full flex items-center justify-center text-[#1a1a1a] transition-colors"
@@ -326,7 +330,7 @@ useEffect(() => {
<div
className="h-full bg-[#f4b840] rounded-full transition-all"
style={{ width: `${progress}%` }}
></div>
/>
</div>
<div className="flex justify-between text-xs text-gray-600">
<span>{formatTime(currentTime)}</span>
@@ -344,10 +348,20 @@ useEffect(() => {
value={volume}
onChange={handleVolumeChange}
className="w-20 h-2 bg-gray-300 rounded-full appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #f4b840 0%, #f4b840 ${volume * 100}%, #d1d5db ${volume * 100}%, #d1d5db 100%)`
}}
/>
</div>
</div>
</div>
{!post.audio_url && (
<p className="text-sm text-gray-500 text-center py-4">
Audio file not available
</p>
)}
</div>
)}
{/* Full Transcript */}

View File

@@ -169,14 +169,7 @@ export default function Search({ user, initialQuery = '', onViewPost}) {
</span>
</>
)}
{result.confidence && (
<>
<span></span>
<span className="text-green-600">
{Math.round(result.confidence * 100)}% confidence
</span>
</>
)}
</div>
</div>
<button