daasime Claude Opus 4.6 commited on
Commit
89a987c
·
1 Parent(s): ac1df35

Fix audio playback + persist logs across refreshes

Browse files

Audio 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>

Files changed (3) hide show
  1. Dockerfile +1 -1
  2. app.py +36 -2
  3. 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