daasime Claude Opus 4.6 commited on
Commit
91ee436
·
1 Parent(s): 69a6bad

Add Test History with View Report in Database tab

Browse files

- Add AnalysisResult.from_dict() to reconstruct results from stored JSON
- Add Test History section in Database tab listing all past analyses
- View Report button loads full report inline with back navigation
- Results now persist across Streamlit reruns via session_state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +50 -0
  2. src/analyzer.py +65 -0
app.py CHANGED
@@ -920,6 +920,16 @@ def render_timeline(result):
920
 
921
  def render_database_tab():
922
  """Render the voiceprint database tab."""
 
 
 
 
 
 
 
 
 
 
923
  st.markdown("### Voiceprint Database")
924
 
925
  analyzer = get_analyzer()
@@ -948,6 +958,46 @@ def render_database_tab():
948
  st.metric("Recurring", stats['multi_appearance'])
949
  st.caption("Voices in 2+ tests")
950
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
951
  # Check if clear was requested
952
  if st.session_state.get('clear_db_search', False):
953
  st.session_state['clear_db_search'] = False
 
920
 
921
  def render_database_tab():
922
  """Render the voiceprint database tab."""
923
+
924
+ # If viewing a historical report, show it first with a back button
925
+ if st.session_state.get('viewing_history') and st.session_state.get('last_result'):
926
+ if st.button("← Back to Database"):
927
+ st.session_state['viewing_history'] = False
928
+ st.session_state['last_result'] = None
929
+ st.rerun()
930
+ render_results(st.session_state['last_result'])
931
+ return
932
+
933
  st.markdown("### Voiceprint Database")
934
 
935
  analyzer = get_analyzer()
 
958
  st.metric("Recurring", stats['multi_appearance'])
959
  st.caption("Voices in 2+ tests")
960
 
961
+ # ---- Test History ----
962
+ st.markdown("#### Test History")
963
+ all_tests = analyzer.db.get_all_tests()
964
+ if all_tests:
965
+ for idx, test in enumerate(all_tests):
966
+ date_str = test.analyzed_at.strftime('%d/%m/%Y %H:%M') if test.analyzed_at else '-'
967
+ risk_data = ""
968
+ if test.results_json:
969
+ try:
970
+ import json as _json
971
+ rj = _json.loads(test.results_json)
972
+ rs = rj.get('risk_score', '-')
973
+ rl = rj.get('risk_label', '')
974
+ risk_icon = {"LOW": "🟢", "MEDIUM": "🟡", "HIGH": "🔴"}.get(rl, "⚪")
975
+ risk_data = f"{risk_icon} {rs}"
976
+ except Exception:
977
+ risk_data = "-"
978
+
979
+ tc1, tc2, tc3, tc4 = st.columns([2, 3, 1, 1.5])
980
+ with tc1:
981
+ st.write(date_str)
982
+ with tc2:
983
+ st.write(test.filename[:40] if test.filename else '-')
984
+ with tc3:
985
+ st.write(risk_data)
986
+ with tc4:
987
+ if st.button("View Report", key=f"view_test_{test.test_id}_{idx}"):
988
+ results = analyzer.db.get_test_results(test.test_id)
989
+ if results:
990
+ from src.analyzer import AnalysisResult
991
+ st.session_state['last_result'] = AnalysisResult.from_dict(results)
992
+ st.session_state['viewing_history'] = True
993
+ st.rerun()
994
+ else:
995
+ st.caption("No tests analyzed yet.")
996
+ st.divider()
997
+
998
+ # ---- Voiceprint Database ----
999
+ st.markdown("#### Voiceprints")
1000
+
1001
  # Check if clear was requested
1002
  if st.session_state.get('clear_db_search', False):
1003
  st.session_state['clear_db_search'] = False
src/analyzer.py CHANGED
@@ -181,6 +181,71 @@ class AnalysisResult:
181
  """Convert to JSON string."""
182
  return json.dumps(self.to_dict(), indent=2)
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  class AudioAnalyzer:
186
  """Main analyzer that orchestrates all phases."""
 
181
  """Convert to JSON string."""
182
  return json.dumps(self.to_dict(), indent=2)
183
 
184
+ @classmethod
185
+ def from_dict(cls, d: dict) -> 'AnalysisResult':
186
+ """Reconstruct an AnalysisResult from a stored dict (e.g. from DB JSON)."""
187
+ main = None
188
+ if d.get('main_speaker'):
189
+ ms = d['main_speaker']
190
+ main = SpeakerResult(
191
+ voiceprint_id=ms.get('voiceprint_id', ''),
192
+ label=ms.get('label', ''),
193
+ role=ms.get('role', 'main'),
194
+ total_seconds=ms.get('total_seconds', 0),
195
+ quality=ms.get('quality', 'Unknown'),
196
+ is_synthetic=ms.get('is_synthetic', False),
197
+ synthetic_score=ms.get('synthetic_score', 0),
198
+ is_playback=ms.get('is_playback', False),
199
+ playback_score=ms.get('playback_score', 0),
200
+ playback_indicators=ms.get('playback_indicators'),
201
+ times_seen=ms.get('times_seen', 1),
202
+ is_flagged=ms.get('is_flagged', False),
203
+ segments=ms.get('segments'),
204
+ clip_path=ms.get('clip_path'),
205
+ )
206
+ additional = []
207
+ for s in d.get('additional_speakers', []):
208
+ additional.append(SpeakerResult(
209
+ voiceprint_id=s.get('voiceprint_id', ''),
210
+ label=s.get('label', ''),
211
+ role=s.get('role', 'additional'),
212
+ total_seconds=s.get('total_seconds', 0),
213
+ quality=s.get('quality', 'Unknown'),
214
+ is_synthetic=s.get('is_synthetic', False),
215
+ synthetic_score=s.get('synthetic_score', 0),
216
+ is_playback=s.get('is_playback', False),
217
+ playback_score=s.get('playback_score', 0),
218
+ playback_indicators=s.get('playback_indicators'),
219
+ times_seen=s.get('times_seen', 1),
220
+ is_flagged=s.get('is_flagged', False),
221
+ segments=s.get('segments'),
222
+ clip_path=s.get('clip_path'),
223
+ ))
224
+ return cls(
225
+ test_id=d.get('test_id', ''),
226
+ filename=d.get('filename', ''),
227
+ duration_seconds=d.get('duration_seconds', 0),
228
+ analyzed_at=d.get('analyzed_at', ''),
229
+ main_speaker=main,
230
+ additional_speakers=additional,
231
+ background_anomalies=d.get('background_anomalies', []),
232
+ wake_words=d.get('wake_words', []),
233
+ assistant_responses=d.get('assistant_responses', []),
234
+ prompt_voice_detected=d.get('prompt_voice_detected', False),
235
+ prompt_voice_seconds=d.get('prompt_voice_seconds', 0),
236
+ playback_detected=d.get('playback_detected', False),
237
+ playback_score=d.get('playback_score', 0),
238
+ playback_indicators=d.get('playback_indicators'),
239
+ whisper_detected=d.get('whisper_detected', False),
240
+ whisper_instances=d.get('whisper_instances'),
241
+ reading_pattern_detected=d.get('reading_pattern_detected', False),
242
+ reading_confidence=d.get('reading_confidence', 0),
243
+ reading_indicators=d.get('reading_indicators'),
244
+ suspicious_pauses_detected=d.get('suspicious_pauses_detected', False),
245
+ suspicious_pauses=d.get('suspicious_pauses'),
246
+ longest_pause=d.get('longest_pause', 0),
247
+ )
248
+
249
 
250
  class AudioAnalyzer:
251
  """Main analyzer that orchestrates all phases."""