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) row = get_audio_post_by_id(post_id)
if not row: if not row:
return _error("Post not found.", 404) return _error("Post not found.", 404)
# CRITICAL: Add audio URL to the response
row = _add_audio_url(row)
return jsonify(row) return jsonify(row)

View File

@@ -150,7 +150,7 @@ class ApiClient {
} }
async getAudioUrl(postId, expiresIn = 3600) { async getAudioUrl(postId, expiresIn = 3600) {
return this.request(`/posts/${postId}/audio?expires_in=${expiresIn}`); return this.request(`/posts/${postId}/audio-url`);
} }
// ==================== Post Files ==================== // ==================== 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 { Clock, Download, ExternalLink } from 'lucide-react'
import { useState, useRef, useEffect } from 'react' import { useState, useEffect } from 'react'
import { api } from '../api' import { api } from '../api'
import AudioPlayer from './AudioPlayer'
export default function AudioPostCard({ post, onViewPost }) { 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 [transcript, setTranscript] = useState(null)
const [loadingTranscript, setLoadingTranscript] = useState(false) const [loadingTranscript, setLoadingTranscript] = useState(false)
const [transcriptExpanded, setTranscriptExpanded] = useState(false)
const [downloading, setDownloading] = useState(false) const [downloading, setDownloading] = useState(false)
const audioRef = useRef(null)
// DEBUG: Log post data to console
useEffect(() => { useEffect(() => {
console.log('Post data:', post) console.log('Post data:', post)
console.log('Audio URL:', post.audio_url) console.log('Audio URL:', post.audio_url)
@@ -29,25 +23,11 @@ export default function AudioPostCard({ post, onViewPost }) {
minute: '2-digit' 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(() => { useEffect(() => {
if (post.status === 'ready' && !transcript && !loadingTranscript) { if (post.status === 'ready' && !transcript && !loadingTranscript) {
loadTranscript() 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]) }, [post.post_id, post.status, post.audio_url])
const loadTranscript = async () => { 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) => { const handleView = (postId) => {
if (onViewPost) { if (onViewPost) {
onViewPost(postId) onViewPost(postId)
} }
} }
const progress = duration > 0 ? (currentTime / duration) * 100 : 0
return ( return (
<article className="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm hover:shadow-md transition-shadow"> <article className="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm hover:shadow-md transition-shadow">
{/* Post Header */} {/* Post Header */}
@@ -239,60 +162,7 @@ export default function AudioPostCard({ post, onViewPost }) {
{post.status === 'ready' && ( {post.status === 'ready' && (
<> <>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200"> <div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
{/* Hidden audio element */} <AudioPlayer src={post.audio_url} />
<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>
</div> </div>
{/* Transcript Section - Always shown */} {/* Transcript Section - Always shown */}

View File

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

View File

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