Skip to content

Commit cf3ed4d

Browse files
committed
fix(transcripts): improve paragraph formatting and set no-store on missing caption responses
1 parent 13de439 commit cf3ed4d

2 files changed

Lines changed: 53 additions & 27 deletions

File tree

‎apps/site/src/components/TranscriptModal.astro‎

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -251,38 +251,53 @@ const {
251251
if (!fullText) {
252252
fulltextView.innerHTML = `<p class="text-muted-foreground italic">No plain text transcript available.</p>`;
253253
} else {
254-
// Split into readable paragraphs (approx every 3-4 sentences or double newlines)
255-
const rawParagraphs = fullText.split(/\n+/).filter(Boolean);
256-
const paragraphs = rawParagraphs.length > 3
257-
? rawParagraphs
258-
: fullText.match(/[^.!?]+[.!?]+(\s+|$)/g) || [fullText];
259-
260-
// Group small sentences into chunky paragraphs of ~4 sentences
261-
const chunky: string[] = [];
262-
let buffer = "";
263-
paragraphs.forEach((p, idx) => {
264-
buffer += (buffer ? " " : "") + p.trim();
265-
if ((idx + 1) % 4 === 0 || idx === paragraphs.length - 1) {
266-
chunky.push(buffer);
267-
buffer = "";
254+
// Build readable paragraphs:
255+
// 1. If cues exist, group cues into natural paragraphs every ~5 cues or 35-40 seconds
256+
// 2. Otherwise split on double newlines or punctuation sentences
257+
let paragraphs: string[] = [];
258+
259+
if (cues.length > 0) {
260+
let currentParagraph: string[] = [];
261+
for (let i = 0; i < cues.length; i++) {
262+
currentParagraph.push(cues[i].text.trim());
263+
if (currentParagraph.length >= 6 || i === cues.length - 1) {
264+
paragraphs.push(currentParagraph.join(" "));
265+
currentParagraph = [];
266+
}
268267
}
269-
});
268+
} else {
269+
const rawParagraphs = fullText.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
270+
if (rawParagraphs.length > 2) {
271+
paragraphs = rawParagraphs;
272+
} else {
273+
// Split by sentence boundaries (. ! ?)
274+
const sentences = fullText.match(/[^.!?]+[.!?]+(?:\s+|$)|[^.!?]+$/g) || [fullText];
275+
let buffer = "";
276+
sentences.forEach((s, idx) => {
277+
buffer += (buffer ? " " : "") + s.trim();
278+
if ((idx + 1) % 4 === 0 || idx === sentences.length - 1) {
279+
paragraphs.push(buffer);
280+
buffer = "";
281+
}
282+
});
283+
}
284+
}
270285

271286
const filtered = filter
272-
? chunky.filter((p) => p.toLowerCase().includes(filter))
273-
: chunky;
287+
? paragraphs.filter((p) => p.toLowerCase().includes(filter))
288+
: paragraphs;
274289

275290
if (filtered.length === 0) {
276291
fulltextView.innerHTML = `<p class="text-muted-foreground italic py-8 text-center">No transcript sections matched "${currentSearch}".</p>`;
277292
} else {
278293
fulltextView.innerHTML = filtered
279294
.map((p) => {
280-
if (!filter) return `<p class="leading-relaxed">${p}</p>`;
295+
if (!filter) return `<p class="leading-relaxed mb-4">${p}</p>`;
281296
const highlighted = p.replace(
282297
new RegExp(`(${filter})`, "gi"),
283298
`<mark class="bg-primary/20 text-primary font-semibold px-0.5 rounded">$1</mark>`,
284299
);
285-
return `<p class="leading-relaxed">${highlighted}</p>`;
300+
return `<p class="leading-relaxed mb-4">${highlighted}</p>`;
286301
})
287302
.join("");
288303
}

‎apps/site/src/pages/api/transcript.ts‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,14 @@ async function fetchYouTubeCaptions(videoId: string): Promise<{
146146
fullText,
147147
status: cues.length > 0 ? "completed" : "no_caption_available",
148148
};
149-
} catch (err) {
149+
} catch (err: any) {
150150
console.warn("[fetchYouTubeCaptions error]", err);
151-
return { cues: [], fullText: "", status: "no_caption_available" };
151+
return {
152+
cues: [],
153+
fullText: "",
154+
status: "no_caption_available",
155+
error: err?.message || String(err),
156+
};
152157
}
153158
}
154159

@@ -312,9 +317,10 @@ export const GET: APIRoute = async ({ request }) => {
312317

313318
// If Sanity doc is missing fullText, but we have a YouTube video ID, fetch on demand
314319
const targetYtId = ytId || transcriptDoc?.youtubeId || parseYoutubeId(youtubeParam);
320+
let captionsResult: any = null;
315321
if (targetYtId) {
316-
const captions = await fetchYouTubeCaptions(targetYtId);
317-
if (captions.status === "completed" && captions.fullText.length > 50) {
322+
captionsResult = await fetchYouTubeCaptions(targetYtId);
323+
if (captionsResult.status === "completed" && captionsResult.fullText.length > 50) {
318324
return new Response(
319325
JSON.stringify(
320326
{
@@ -324,9 +330,9 @@ export const GET: APIRoute = async ({ request }) => {
324330
duration: transcriptDoc?.duration || "",
325331
durationSeconds: transcriptDoc?.durationSeconds || 0,
326332
summary: transcriptDoc?.summary || "",
327-
fullText: captions.fullText,
333+
fullText: captionsResult.fullText,
328334
chapters: transcriptDoc?.chapters || [],
329-
cues: captions.cues,
335+
cues: captionsResult.cues,
330336
status: "completed",
331337
},
332338
null,
@@ -336,7 +342,7 @@ export const GET: APIRoute = async ({ request }) => {
336342
status: 200,
337343
headers: {
338344
...CORS_HEADERS,
339-
"cache-control": "public, max-age=3600, s-maxage=86400",
345+
"cache-control": "public, max-age=86400, s-maxage=604800",
340346
},
341347
},
342348
);
@@ -360,6 +366,11 @@ export const GET: APIRoute = async ({ request }) => {
360366
chapters: transcriptDoc.chapters || [],
361367
cues: transcriptDoc.cues || [],
362368
status: transcriptDoc.status || "no_caption_available",
369+
_debug: {
370+
targetYtId,
371+
captionsStatus: captionsResult?.status,
372+
captionsError: captionsResult?.error,
373+
},
363374
},
364375
null,
365376
2,
@@ -368,7 +379,7 @@ export const GET: APIRoute = async ({ request }) => {
368379
status: 200,
369380
headers: {
370381
...CORS_HEADERS,
371-
"cache-control": "public, max-age=1800",
382+
"cache-control": "no-store, no-cache, must-revalidate",
372383
},
373384
},
374385
);

0 commit comments

Comments
 (0)