Spaces:
Running
Running
Fix audio playback + persist logs across refreshes
Browse filesAudio clips: Convert from float32 WAV to PCM int16 WAV for browser
compatibility. Float32 WAV causes playback to cut short or fail
in many browsers.
Logs: Save session history to JSON file on disk (DATA_DIR/logs/)
so logs survive page refreshes and container restarts. Load from
disk on init, clear button also wipes the file.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Dockerfile +1 -1
- app.py +36 -2
- src/analyzer.py +6 -2
Dockerfile
CHANGED
|
@@ -20,7 +20,7 @@ RUN useradd -m -u 1000 user
|
|
| 20 |
|
| 21 |
# Create model & data dirs under /home/user (NOT /app, which HF mounts over)
|
| 22 |
RUN mkdir -p /home/user/models/vad /home/user/models/spkrec \
|
| 23 |
-
/home/user/data/db /home/user/data/clips \
|
| 24 |
&& chown -R user:user /home/user
|
| 25 |
|
| 26 |
# Pre-download models AS the user so files are owned by user
|
|
|
|
| 20 |
|
| 21 |
# Create model & data dirs under /home/user (NOT /app, which HF mounts over)
|
| 22 |
RUN mkdir -p /home/user/models/vad /home/user/models/spkrec \
|
| 23 |
+
/home/user/data/db /home/user/data/clips /home/user/data/logs \
|
| 24 |
&& chown -R user:user /home/user
|
| 25 |
|
| 26 |
# Pre-download models AS the user so files are owned by user
|
app.py
CHANGED
|
@@ -86,12 +86,44 @@ def get_analyzer():
|
|
| 86 |
return AudioAnalyzer()
|
| 87 |
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
def _init_log_state():
|
| 90 |
-
"""Initialize session state for analysis logs."""
|
| 91 |
if 'analysis_logs' not in st.session_state:
|
| 92 |
st.session_state['analysis_logs'] = {
|
| 93 |
'current': [],
|
| 94 |
-
'sessions':
|
| 95 |
}
|
| 96 |
|
| 97 |
|
|
@@ -131,6 +163,7 @@ def _finalize_log_session(result=None):
|
|
| 131 |
}
|
| 132 |
logs['sessions'].insert(0, session)
|
| 133 |
logs['current'] = []
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
def render_waveform(audio_data, sample_rate):
|
|
@@ -1123,6 +1156,7 @@ def render_logs_tab():
|
|
| 1123 |
|
| 1124 |
if st.button("🗑️ Clear History", key="clear_log_history"):
|
| 1125 |
st.session_state['analysis_logs']['sessions'] = []
|
|
|
|
| 1126 |
st.rerun()
|
| 1127 |
|
| 1128 |
|
|
|
|
| 86 |
return AudioAnalyzer()
|
| 87 |
|
| 88 |
|
| 89 |
+
def _get_logs_file():
|
| 90 |
+
"""Path to the persistent logs JSON file."""
|
| 91 |
+
data_dir = os.environ.get("DATA_DIR", "data")
|
| 92 |
+
logs_dir = os.path.join(data_dir, "logs")
|
| 93 |
+
os.makedirs(logs_dir, exist_ok=True)
|
| 94 |
+
return os.path.join(logs_dir, "analysis_logs.json")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _load_persisted_logs():
|
| 98 |
+
"""Load log sessions from disk."""
|
| 99 |
+
import json as _json
|
| 100 |
+
path = _get_logs_file()
|
| 101 |
+
if os.path.exists(path):
|
| 102 |
+
try:
|
| 103 |
+
with open(path, 'r') as f:
|
| 104 |
+
return _json.load(f)
|
| 105 |
+
except Exception:
|
| 106 |
+
pass
|
| 107 |
+
return []
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _save_persisted_logs(sessions):
|
| 111 |
+
"""Save log sessions to disk."""
|
| 112 |
+
import json as _json
|
| 113 |
+
path = _get_logs_file()
|
| 114 |
+
try:
|
| 115 |
+
with open(path, 'w') as f:
|
| 116 |
+
_json.dump(sessions, f)
|
| 117 |
+
except Exception:
|
| 118 |
+
pass
|
| 119 |
+
|
| 120 |
+
|
| 121 |
def _init_log_state():
|
| 122 |
+
"""Initialize session state for analysis logs, loading from disk."""
|
| 123 |
if 'analysis_logs' not in st.session_state:
|
| 124 |
st.session_state['analysis_logs'] = {
|
| 125 |
'current': [],
|
| 126 |
+
'sessions': _load_persisted_logs()
|
| 127 |
}
|
| 128 |
|
| 129 |
|
|
|
|
| 163 |
}
|
| 164 |
logs['sessions'].insert(0, session)
|
| 165 |
logs['current'] = []
|
| 166 |
+
_save_persisted_logs(logs['sessions'])
|
| 167 |
|
| 168 |
|
| 169 |
def render_waveform(audio_data, sample_rate):
|
|
|
|
| 1156 |
|
| 1157 |
if st.button("🗑️ Clear History", key="clear_log_history"):
|
| 1158 |
st.session_state['analysis_logs']['sessions'] = []
|
| 1159 |
+
_save_persisted_logs([])
|
| 1160 |
st.rerun()
|
| 1161 |
|
| 1162 |
|
src/analyzer.py
CHANGED
|
@@ -646,11 +646,15 @@ class AudioAnalyzer:
|
|
| 646 |
# Concatenate all clips
|
| 647 |
clip = torch.cat(clips, dim=1)
|
| 648 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
# Save clip
|
|
|
|
| 650 |
clip_filename = f"{test_id}_{vp_id}_{total_duration:.1f}s.wav"
|
| 651 |
clip_path = os.path.join(self.clips_dir, clip_filename)
|
| 652 |
-
|
| 653 |
-
torchaudio.save(clip_path, clip, sample_rate)
|
| 654 |
|
| 655 |
return clip_path
|
| 656 |
|
|
|
|
| 646 |
# Concatenate all clips
|
| 647 |
clip = torch.cat(clips, dim=1)
|
| 648 |
|
| 649 |
+
# Convert to int16 PCM for browser compatibility
|
| 650 |
+
clip_np = clip.squeeze(0).numpy()
|
| 651 |
+
clip_int16 = np.clip(clip_np * 32767, -32768, 32767).astype(np.int16)
|
| 652 |
+
|
| 653 |
# Save clip
|
| 654 |
+
import soundfile as sf
|
| 655 |
clip_filename = f"{test_id}_{vp_id}_{total_duration:.1f}s.wav"
|
| 656 |
clip_path = os.path.join(self.clips_dir, clip_filename)
|
| 657 |
+
sf.write(clip_path, clip_int16, sample_rate, subtype='PCM_16')
|
|
|
|
| 658 |
|
| 659 |
return clip_path
|
| 660 |
|