diff --git a/backend/__pycache__/api_routes.cpython-314.pyc b/backend/__pycache__/api_routes.cpython-314.pyc
index 78e5b27..ee62c8f 100644
Binary files a/backend/__pycache__/api_routes.cpython-314.pyc and b/backend/__pycache__/api_routes.cpython-314.pyc differ
diff --git a/backend/api_routes.py b/backend/api_routes.py
index 4575134..7ad8841 100644
--- a/backend/api_routes.py
+++ b/backend/api_routes.py
@@ -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)
diff --git a/backend/uploads/2b06e3ac-4366-45c9-bfb5-cf7856392a51_data.m4a b/backend/uploads/2b06e3ac-4366-45c9-bfb5-cf7856392a51_data.m4a
new file mode 100644
index 0000000..38e5740
Binary files /dev/null and b/backend/uploads/2b06e3ac-4366-45c9-bfb5-cf7856392a51_data.m4a differ
diff --git a/frontend/src/api.js b/frontend/src/api.js
index 9cbc904..2add92c 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -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 ====================
diff --git a/frontend/src/components/AudioPlayer.jsx b/frontend/src/components/AudioPlayer.jsx
new file mode 100644
index 0000000..69265e6
--- /dev/null
+++ b/frontend/src/components/AudioPlayer.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+ {formatTime(currentTime)}
+ {formatTime(duration)}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/AudioPostCard.jsx b/frontend/src/components/AudioPostCard.jsx
index 7c4a252..8b9659f 100644
--- a/frontend/src/components/AudioPostCard.jsx
+++ b/frontend/src/components/AudioPostCard.jsx
@@ -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 (
{/* Post Header */}
@@ -239,60 +162,7 @@ export default function AudioPostCard({ post, onViewPost }) {
{post.status === 'ready' && (
<>
- {/* Hidden audio element */}
-
{/* Transcript Section - Always shown */}
diff --git a/frontend/src/pages/PostDetail.jsx b/frontend/src/pages/PostDetail.jsx
index 09994b4..bfa51b3 100644
--- a/frontend/src/pages/PostDetail.jsx
+++ b/frontend/src/pages/PostDetail.jsx
@@ -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
-useEffect(() => {
- if (audioRef.current && post?.audio_url) {
- console.log('Setting audio src:', post.audio_url)
- audioRef.current.src = post.audio_url
- audioRef.current.load() // Force reload
- }
-}, [post?.audio_url])
-
+ // Set audio source when post loads
+ useEffect(() => {
+ if (post?.audio_url && audioRef.current) {
+ console.log('Setting audio source:', post.audio_url)
+ audioRef.current.src = post.audio_url
+ 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(() => {
{/* Audio Player */}
- {post.status === 'ready' && (
+ {post.status === 'ready' && post.audio_url && (
Audio Player
@@ -303,50 +303,64 @@ useEffect(() => {
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onEnded={handleEnded}
+ onError={handleAudioError}
+ onCanPlay={() => console.log('Audio can play')}
preload="metadata"
+ crossOrigin="anonymous"
/>
-
-
-
-
-
+
-
- {formatTime(currentTime)}
- {formatTime(duration)}
-
-
+ {isPlaying ? (
+
+ ) : (
+
+ )}
+
-
-
-
+
+
+
+ {formatTime(currentTime)}
+ {formatTime(duration)}
+
+
+
+
+
+
+
+
+ {!post.audio_url && (
+
+ Audio file not available
+
+ )}
)}
diff --git a/frontend/src/pages/Search.jsx b/frontend/src/pages/Search.jsx
index 831e7bb..c0ef850 100644
--- a/frontend/src/pages/Search.jsx
+++ b/frontend/src/pages/Search.jsx
@@ -169,14 +169,7 @@ export default function Search({ user, initialQuery = '', onViewPost}) {
>
)}
- {result.confidence && (
- <>
-
•
-
- {Math.round(result.confidence * 100)}% confidence
-
- >
- )}
+