danielrosehill Claude commited on
Commit
6f4ed59
·
1 Parent(s): b5a4032

Redesign STT comparison to side-by-side table layout

Browse files

- Initialize Git LFS for audio file tracking
- Replace vertical model cards with horizontal comparison table
- Display ground truth alongside models in single row per transcript
- Add sticky header with model names and color coding
- Implement active row highlighting during audio playback
- Add auto-scroll to follow current segment
- Improve mobile responsiveness with horizontal scroll
- Simplify comparison by synchronizing all transcripts to ground truth timeline

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

Files changed (3) hide show
  1. .gitattributes +1 -0
  2. index.html +93 -85
  3. style.css +91 -85
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.mp3 filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.mp3 filter=lfs diff=lfs merge=lfs -text
37
+ *.wav filter=lfs diff=lfs merge=lfs -text
index.html CHANGED
@@ -12,8 +12,8 @@
12
  <div>
13
  <h1>Speech-to-Text Comparison</h1>
14
  <p>
15
- Play the sample podcast and compare how each transcription model handled it.
16
- The ground-truth reference stays on top in green so you can quickly gauge accuracy.
17
  </p>
18
  </div>
19
  <div class="audio-shell">
@@ -22,14 +22,12 @@
22
  </div>
23
  </section>
24
  <section class="transcripts">
25
- <div id="reference-track" aria-live="polite"></div>
26
- <div class="models-grid" id="models-grid" aria-live="polite"></div>
27
  </section>
28
  </main>
29
  <script src="transcripts.js"></script>
30
  <script type="module">
31
- const referenceTrackEl = document.getElementById("reference-track");
32
- const modelsGridEl = document.getElementById("models-grid");
33
  const audioElem = document.getElementById("audio");
34
  const waveformCanvas = document.getElementById("waveform");
35
  const transcriptSources = window.TRANSCRIPTS || {};
@@ -38,8 +36,7 @@
38
  id: "truth",
39
  label: "Ground Truth",
40
  file: "data/ground-truth/truth_1.srt",
41
- accent: "#00b894",
42
- emphasis: true
43
  },
44
  {
45
  id: "assembly",
@@ -67,7 +64,8 @@
67
  }
68
  ];
69
 
70
- const segmentNodes = [];
 
71
 
72
  function parseTimestamp(value) {
73
  const [time, millisecondPart] = value.split(",");
@@ -90,16 +88,6 @@
90
  .filter(Boolean);
91
  }
92
 
93
- function createSegmentElement(segment, accent) {
94
- const segmentEl = document.createElement("div");
95
- segmentEl.className = "segment";
96
- segmentEl.dataset.start = segment.start;
97
- segmentEl.dataset.end = segment.end;
98
- segmentEl.innerHTML = `<span class="segment-time">${formatTime(segment.start)}</span><p>${segment.content}</p>`;
99
- segmentEl.style.setProperty("--accent", accent);
100
- return segmentEl;
101
- }
102
-
103
  function formatTime(seconds) {
104
  const minutes = Math.floor(seconds / 60)
105
  .toString()
@@ -126,54 +114,98 @@
126
  return (await response.text()).replace(/^\ufeff/, "");
127
  }
128
 
129
- async function loadTrack(track) {
130
- let transcriptText = getTranscriptText(track);
131
- if (!transcriptText) {
132
- try {
133
- transcriptText = await fetchTranscript(track);
134
- } catch (error) {
135
- renderFallbackCard(track, error.message);
136
- throw error;
137
- }
138
- }
139
- const transcript = parseSrt(transcriptText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- const trackEl = document.createElement("article");
142
- trackEl.className = "track";
143
- if (track.emphasis) {
144
- trackEl.classList.add("track--emphasis");
145
- }
146
- trackEl.style.setProperty("--accent", track.accent);
147
 
148
- trackEl.innerHTML = `
149
- <header>
150
- <h2>${track.label}</h2>
151
- <span class="badge">Segments: ${transcript.length}</span>
152
- </header>
 
 
153
  `;
 
154
 
155
- const contentEl = document.createElement("div");
156
- contentEl.className = "track-body";
157
- transcript.forEach((segment) => {
158
- const segmentEl = createSegmentElement(segment, track.accent);
159
- contentEl.appendChild(segmentEl);
160
- segmentNodes.push(segmentEl);
161
- });
162
 
163
- trackEl.appendChild(contentEl);
164
- if (track.emphasis) {
165
- referenceTrackEl.appendChild(trackEl);
166
- } else {
167
- modelsGridEl.appendChild(trackEl);
168
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  }
170
 
171
  function updateActiveSegments(time) {
172
- segmentNodes.forEach((node) => {
173
- const start = Number(node.dataset.start);
174
- const end = Number(node.dataset.end);
175
- const isActive = time >= start && time <= end;
176
- node.classList.toggle("is-active", isActive);
 
 
 
 
 
177
  });
178
  }
179
 
@@ -212,35 +244,11 @@
212
  }
213
 
214
  async function bootstrap() {
215
- await Promise.all(
216
- tracks.map(async (track) => {
217
- try {
218
- await loadTrack(track);
219
- } catch (error) {
220
- console.error(error);
221
- }
222
- })
223
- );
224
  void drawWaveform();
225
  }
226
 
227
- function renderFallbackCard(track, message) {
228
- const card = document.createElement("article");
229
- card.className = "track track--error";
230
- card.innerHTML = `
231
- <header>
232
- <h2>${track.label}</h2>
233
- <span class="badge badge--error">Unavailable</span>
234
- </header>
235
- <p class="track-error">${message}</p>
236
- `;
237
- if (track.emphasis) {
238
- referenceTrackEl.appendChild(card);
239
- } else {
240
- modelsGridEl.appendChild(card);
241
- }
242
- }
243
-
244
  audioElem.addEventListener("timeupdate", () => updateActiveSegments(audioElem.currentTime));
245
  window.addEventListener("resize", () => drawWaveform());
246
 
 
12
  <div>
13
  <h1>Speech-to-Text Comparison</h1>
14
  <p>
15
+ Play the sample podcast and compare how each transcription model handled it side-by-side.
16
+ Each row shows all transcripts for the same time segment.
17
  </p>
18
  </div>
19
  <div class="audio-shell">
 
22
  </div>
23
  </section>
24
  <section class="transcripts">
25
+ <div class="comparison-table" id="comparison-table" aria-live="polite"></div>
 
26
  </section>
27
  </main>
28
  <script src="transcripts.js"></script>
29
  <script type="module">
30
+ const comparisonTableEl = document.getElementById("comparison-table");
 
31
  const audioElem = document.getElementById("audio");
32
  const waveformCanvas = document.getElementById("waveform");
33
  const transcriptSources = window.TRANSCRIPTS || {};
 
36
  id: "truth",
37
  label: "Ground Truth",
38
  file: "data/ground-truth/truth_1.srt",
39
+ accent: "#00b894"
 
40
  },
41
  {
42
  id: "assembly",
 
64
  }
65
  ];
66
 
67
+ const segmentRows = [];
68
+ let allTranscripts = {};
69
 
70
  function parseTimestamp(value) {
71
  const [time, millisecondPart] = value.split(",");
 
88
  .filter(Boolean);
89
  }
90
 
 
 
 
 
 
 
 
 
 
 
91
  function formatTime(seconds) {
92
  const minutes = Math.floor(seconds / 60)
93
  .toString()
 
114
  return (await response.text()).replace(/^\ufeff/, "");
115
  }
116
 
117
+ async function loadAllTranscripts() {
118
+ const results = {};
119
+ await Promise.all(
120
+ tracks.map(async (track) => {
121
+ try {
122
+ let transcriptText = getTranscriptText(track);
123
+ if (!transcriptText) {
124
+ transcriptText = await fetchTranscript(track);
125
+ }
126
+ results[track.id] = {
127
+ segments: parseSrt(transcriptText),
128
+ track: track
129
+ };
130
+ } catch (error) {
131
+ console.error(`Failed to load ${track.label}:`, error);
132
+ results[track.id] = {
133
+ segments: [],
134
+ track: track,
135
+ error: true
136
+ };
137
+ }
138
+ })
139
+ );
140
+ return results;
141
+ }
142
 
143
+ function findSegmentForTime(segments, time) {
144
+ return segments.find(seg => time >= seg.start && time < seg.end);
145
+ }
 
 
 
146
 
147
+ function renderComparisonTable() {
148
+ // Create header row
149
+ const header = document.createElement("div");
150
+ header.className = "comparison-header";
151
+ header.innerHTML = `
152
+ <div class="time-column">Time</div>
153
+ ${tracks.map(track => `<div class="model-column" style="--accent: ${track.accent}">${track.label}</div>`).join("")}
154
  `;
155
+ comparisonTableEl.appendChild(header);
156
 
157
+ // Use ground truth as the base timeline
158
+ const groundTruthSegments = allTranscripts.truth?.segments || [];
 
 
 
 
 
159
 
160
+ groundTruthSegments.forEach((truthSegment, index) => {
161
+ const row = document.createElement("div");
162
+ row.className = "comparison-row";
163
+ row.dataset.start = truthSegment.start;
164
+ row.dataset.end = truthSegment.end;
165
+
166
+ // Time column
167
+ const timeCell = document.createElement("div");
168
+ timeCell.className = "time-cell";
169
+ timeCell.textContent = formatTime(truthSegment.start);
170
+ row.appendChild(timeCell);
171
+
172
+ // Add cells for each model
173
+ tracks.forEach(track => {
174
+ const cell = document.createElement("div");
175
+ cell.className = "transcript-cell";
176
+ cell.style.setProperty("--accent", track.accent);
177
+
178
+ const transcript = allTranscripts[track.id];
179
+ if (transcript?.error) {
180
+ cell.innerHTML = '<em class="error-text">Error loading</em>';
181
+ } else {
182
+ // Find the segment that overlaps with this ground truth time
183
+ const segment = findSegmentForTime(
184
+ transcript?.segments || [],
185
+ truthSegment.start
186
+ );
187
+ cell.textContent = segment?.content || "—";
188
+ }
189
+
190
+ row.appendChild(cell);
191
+ });
192
+
193
+ comparisonTableEl.appendChild(row);
194
+ segmentRows.push(row);
195
+ });
196
  }
197
 
198
  function updateActiveSegments(time) {
199
+ segmentRows.forEach((row) => {
200
+ const start = Number(row.dataset.start);
201
+ const end = Number(row.dataset.end);
202
+ const isActive = time >= start && time < end;
203
+ row.classList.toggle("is-active", isActive);
204
+
205
+ // Auto-scroll to active row
206
+ if (isActive) {
207
+ row.scrollIntoView({ behavior: "smooth", block: "center" });
208
+ }
209
  });
210
  }
211
 
 
244
  }
245
 
246
  async function bootstrap() {
247
+ allTranscripts = await loadAllTranscripts();
248
+ renderComparisonTable();
 
 
 
 
 
 
 
249
  void drawWaveform();
250
  }
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  audioElem.addEventListener("timeupdate", () => updateActiveSegments(audioElem.currentTime));
253
  window.addEventListener("resize", () => drawWaveform());
254
 
style.css CHANGED
@@ -72,118 +72,96 @@ audio {
72
  gap: 1.75rem;
73
  }
74
 
75
- #reference-track {
76
- display: grid;
77
- }
78
-
79
- .models-grid {
80
- display: grid;
81
- gap: 1.25rem;
82
- }
83
-
84
- .track {
85
  background: #ffffff;
86
  border-radius: 20px;
87
- padding: 1.25rem;
88
  box-shadow: 0 20px 40px rgba(15, 23, 42, 0.07);
89
  border: 1px solid rgba(31, 41, 55, 0.08);
 
90
  }
91
 
92
- .track--error {
93
- border: 1px dashed rgba(239, 68, 68, 0.6);
94
- background: rgba(254, 242, 242, 0.9);
95
- box-shadow: none;
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
97
 
98
- .track--emphasis {
99
- border: 2px solid #00b894;
100
- box-shadow: 0 25px 45px rgba(0, 184, 148, 0.15);
101
  }
102
 
103
- .track header {
104
- display: flex;
105
- flex-wrap: wrap;
106
- align-items: center;
107
- justify-content: space-between;
108
- gap: 0.75rem;
109
- margin-bottom: 1rem;
110
  }
111
 
112
- .track h2 {
113
- font-size: 1.25rem;
114
- margin: 0;
 
 
 
115
  }
116
 
117
- .badge {
118
- background: rgba(31, 41, 55, 0.05);
119
- color: #1f2937;
120
- padding: 0.2rem 0.75rem;
121
- border-radius: 999px;
122
- font-size: 0.85rem;
123
  }
124
 
125
- .badge--error {
126
- background: rgba(239, 68, 68, 0.15);
127
- color: #b91c1c;
 
 
 
 
128
  }
129
 
130
- .track-error {
131
- margin: 0;
132
- color: #b91c1c;
 
133
  font-weight: 500;
 
 
134
  }
135
 
136
- .track-body {
137
- display: grid;
138
- gap: 0.5rem;
139
- max-height: 300px;
140
- overflow: auto;
141
- scrollbar-width: thin;
142
- }
143
-
144
- .track-body::-webkit-scrollbar {
145
- width: 6px;
146
- }
147
-
148
- .track-body::-webkit-scrollbar-thumb {
149
- background: rgba(31, 41, 55, 0.25);
150
- border-radius: 999px;
151
- }
152
-
153
- .segment {
154
- padding: 0.85rem 1rem;
155
- border-radius: 14px;
156
- background: rgba(15, 23, 42, 0.03);
157
- border: 1px solid rgba(0, 0, 0, 0.04);
158
- transition: background 0.25s ease, transform 0.25s ease, border 0.25s ease;
159
  }
160
 
161
- .segment p {
162
- margin-top: 0.35rem;
163
- margin-bottom: 0;
164
- color: #111827;
165
  }
166
 
167
- .segment-time {
168
- display: inline-flex;
169
- align-items: center;
170
- gap: 0.35rem;
171
- font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
172
- font-size: 0.85rem;
173
- color: rgba(55, 65, 81, 0.9);
174
  }
175
 
176
- .segment.is-active {
177
- background: rgba(64, 112, 244, 0.08);
178
- border-color: var(--accent, rgba(64, 112, 244, 0.5));
179
- box-shadow: 0 10px 20px rgba(64, 112, 244, 0.15);
180
- transform: translateY(-2px);
181
  }
182
 
183
- .track--emphasis .segment.is-active {
184
- background: rgba(0, 184, 148, 0.1);
185
- box-shadow: 0 12px 24px rgba(0, 184, 148, 0.2);
186
- border-color: rgba(0, 184, 148, 0.6);
187
  }
188
 
189
  @media (min-width: 720px) {
@@ -192,7 +170,35 @@ audio {
192
  align-items: center;
193
  }
194
 
195
- .models-grid {
196
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  }
198
  }
 
72
  gap: 1.75rem;
73
  }
74
 
75
+ .comparison-table {
 
 
 
 
 
 
 
 
 
76
  background: #ffffff;
77
  border-radius: 20px;
 
78
  box-shadow: 0 20px 40px rgba(15, 23, 42, 0.07);
79
  border: 1px solid rgba(31, 41, 55, 0.08);
80
+ overflow: hidden;
81
  }
82
 
83
+ .comparison-header {
84
+ display: grid;
85
+ grid-template-columns: 80px repeat(5, 1fr);
86
+ gap: 1px;
87
+ background: rgba(15, 23, 42, 0.05);
88
+ font-weight: 600;
89
+ font-size: 0.9rem;
90
+ position: sticky;
91
+ top: 0;
92
+ z-index: 10;
93
+ }
94
+
95
+ .time-column,
96
+ .model-column {
97
+ padding: 0.85rem 1rem;
98
+ background: #ffffff;
99
  }
100
 
101
+ .time-column {
102
+ font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
103
+ color: rgba(55, 65, 81, 0.9);
104
  }
105
 
106
+ .model-column {
107
+ color: var(--accent, #1f2937);
108
+ text-align: left;
109
+ border-left: 3px solid var(--accent);
 
 
 
110
  }
111
 
112
+ .comparison-row {
113
+ display: grid;
114
+ grid-template-columns: 80px repeat(5, 1fr);
115
+ gap: 1px;
116
+ background: rgba(15, 23, 42, 0.03);
117
+ transition: background 0.25s ease, transform 0.25s ease;
118
  }
119
 
120
+ .comparison-row:hover {
121
+ background: rgba(15, 23, 42, 0.06);
 
 
 
 
122
  }
123
 
124
+ .time-cell,
125
+ .transcript-cell {
126
+ padding: 1rem;
127
+ background: #ffffff;
128
+ font-size: 0.9rem;
129
+ line-height: 1.6;
130
+ color: #1f2937;
131
  }
132
 
133
+ .time-cell {
134
+ font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
135
+ font-size: 0.85rem;
136
+ color: rgba(55, 65, 81, 0.9);
137
  font-weight: 500;
138
+ display: flex;
139
+ align-items: center;
140
  }
141
 
142
+ .transcript-cell {
143
+ border-left: 3px solid transparent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  }
145
 
146
+ .comparison-row.is-active {
147
+ background: linear-gradient(90deg, rgba(64, 112, 244, 0.08), rgba(64, 112, 244, 0.03));
148
+ transform: scale(1.01);
149
+ box-shadow: 0 4px 12px rgba(64, 112, 244, 0.12);
150
  }
151
 
152
+ .comparison-row.is-active .transcript-cell {
153
+ border-left-color: var(--accent);
154
+ font-weight: 500;
 
 
 
 
155
  }
156
 
157
+ .comparison-row.is-active .time-cell {
158
+ color: #4070f4;
159
+ font-weight: 700;
 
 
160
  }
161
 
162
+ .error-text {
163
+ color: #b91c1c;
164
+ font-style: italic;
 
165
  }
166
 
167
  @media (min-width: 720px) {
 
170
  align-items: center;
171
  }
172
 
173
+ .comparison-table {
174
+ max-height: 70vh;
175
+ overflow-y: auto;
176
+ scrollbar-width: thin;
177
+ }
178
+
179
+ .comparison-table::-webkit-scrollbar {
180
+ width: 8px;
181
+ }
182
+
183
+ .comparison-table::-webkit-scrollbar-thumb {
184
+ background: rgba(31, 41, 55, 0.25);
185
+ border-radius: 999px;
186
+ }
187
+ }
188
+
189
+ @media (max-width: 719px) {
190
+ .comparison-header,
191
+ .comparison-row {
192
+ grid-template-columns: 60px repeat(5, minmax(120px, 1fr));
193
+ font-size: 0.85rem;
194
+ }
195
+
196
+ .time-cell,
197
+ .transcript-cell {
198
+ padding: 0.75rem 0.5rem;
199
+ }
200
+
201
+ .comparison-table {
202
+ overflow-x: auto;
203
  }
204
  }