Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Clears the current user session (logout).
import { NextResponse } from "next/server";
import { clearSession } from "@/lib/auth/service";

Expand Down
1 change: 1 addition & 0 deletions app/api/auth/me/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Returns the authenticated user's email and feature flags.
import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth/service";

Expand Down
1 change: 1 addition & 0 deletions app/api/auth/request-link/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Sends a magic-link login email to the given address.
import { NextResponse } from "next/server";
import { getTranslations } from "next-intl/server";
import { sendMagicLink } from "@/lib/auth/mail";
Expand Down
1 change: 1 addition & 0 deletions app/api/auth/verify/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Verifies a magic-link token and creates an authenticated session.
import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
import { getTranslations } from "next-intl/server";
Expand Down
1 change: 1 addition & 0 deletions app/api/cron/check-pv/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Cron: checks for newly available PV documents on recent meetings.
import { NextRequest, NextResponse } from "next/server";
import { runCheckPv } from "@/lib/cron/check-pv";
import { apiError } from "@/lib/api-error";
Expand Down
1 change: 1 addition & 0 deletions app/api/cron/liveness-sweep/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Cron: marks heartbeat-stale transcript rows as interrupted.
import { NextRequest, NextResponse } from "next/server";
import { runLivenessSweep } from "@/lib/cron/liveness-sweep";
import { apiError } from "@/lib/api-error";
Expand Down
1 change: 1 addition & 0 deletions app/api/cron/process-scheduled/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Cron: starts transcription for scheduled and interrupted transcript rows.
import { NextRequest, NextResponse } from "next/server";
import { runProcessScheduled } from "@/lib/cron/process-scheduled";
import { apiError } from "@/lib/api-error";
Expand Down
1 change: 1 addition & 0 deletions app/api/cron/realign/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Cron: recalculates timestamp offsets for re-cut videos.
import { NextRequest, NextResponse } from "next/server";
import { runRealign } from "@/lib/cron/realign";
import { apiError } from "@/lib/api-error";
Expand Down
1 change: 1 addition & 0 deletions app/api/cron/send-transcript-notifications/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Cron: emails subscribers when a requested transcript is ready.
import { NextRequest, NextResponse } from "next/server";
import { runSendTranscriptNotifications } from "@/lib/cron/send-transcript-notifications";
import { apiError } from "@/lib/api-error";
Expand Down
1 change: 1 addition & 0 deletions app/api/cron/sync-videos/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Cron: scrapes UN Web TV and upserts meeting records.
import { NextRequest, NextResponse } from "next/server";
import { runSyncVideos } from "@/lib/cron/sync-videos";
import { apiError } from "@/lib/api-error";
Expand Down
42 changes: 25 additions & 17 deletions app/api/data/[locale]/[format]/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
// Public data API: meeting list and single-meeting detail as JSON or plain text.
import { routing } from "@/i18n/routing";
import {
getCachedTranscriptedEntries,
getCachedTranscriptedEntriesByLanguage,
} from "@/lib/cached-db";
import { compressedJson, compressedText } from "@/lib/compressed-json";
import { TRANSCRIPT_DISCLAIMER } from "@/lib/config";
import { getCountryName } from "@/lib/country-lookup";
import {
getTranscriptByKalturaId,
getVideoByAssetId,
getVideoByCitation,
getTranscriptByKalturaId,
queryVideos,
type VideoRecord,
type VideosQueryParams,
} from "@/lib/db";
import { symbolFromSlug } from "@/lib/meeting-slug";
import {
getCachedTranscriptedEntries,
getCachedTranscriptedEntriesByLanguage,
} from "@/lib/cached-db";
import { getVideoMetadata, recordToVideo } from "@/lib/un-api";
import {
formatSpeakerInfo,
getSpeakerMapping,
SpeakerInfo,
formatSpeakerInfo,
} from "@/lib/speakers";
import { getCountryName } from "@/lib/country-lookup";
import { symbolFromSlug } from "@/lib/meeting-slug";
import { videoUrl } from "@/lib/video-url";
import { TRANSCRIPT_DISCLAIMER } from "@/lib/config";
import { routing } from "@/i18n/routing";
import { compressedJson, compressedText } from "@/lib/compressed-json";
import {
buildSpeakerSegments,
formatTranscriptAsPlainText,
formatSpeakerText,
formatTimecode,
formatTranscriptAsPlainText,
} from "@/lib/transcript-formatting";
import { getVideoMetadata, recordToVideo } from "@/lib/un-api";
import { videoUrl } from "@/lib/video-url";
import { NextRequest, NextResponse } from "next/server";

// Unified data-API handler. The proxy (proxy.ts) rewrites
// /{locale}/{slug}.json → /api/data/{locale}/json/{slug}
Expand Down Expand Up @@ -167,8 +168,7 @@ async function handleMeeting(
if (format === "text") {
return textResponse(
request,
buildHeader(locale, video, record, null) +
"No transcript available.\n",
buildHeader(locale, video, record, null) + "No transcript available.\n",
);
}
const metadata = await getVideoMetadata(record.asset_id);
Expand Down Expand Up @@ -377,6 +377,12 @@ async function handleList(
const dateRaw = sp.get("date");
const date =
dateRaw && /^\d{4}-\d{2}-\d{2}$/.test(dateRaw) ? dateRaw : undefined;
const fromRaw = sp.get("from");
const dateFrom =
fromRaw && /^\d{4}-\d{2}-\d{2}$/.test(fromRaw) ? fromRaw : undefined;
const toRaw = sp.get("to");
const dateTo =
toRaw && /^\d{4}-\d{2}-\d{2}$/.test(toRaw) ? toRaw : undefined;
const docs = sp
Comment on lines 377 to 386
.getAll("text")
.filter((d) => ["transcript", "pv", "sr"].includes(d));
Expand All @@ -394,6 +400,8 @@ async function handleList(
q,
daysBack: LIST_DAYS_BACK,
date,
dateFrom,
dateTo,
category: sp.get("category") || undefined,
docs: docs.length ? docs : undefined,
sort: { by, dir },
Expand Down
1 change: 1 addition & 0 deletions app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Database ping; returns 200 ok or 503 error.
import { pool } from "@/lib/db";

export async function GET() {
Expand Down
1 change: 1 addition & 0 deletions app/api/languages/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Lists available audio language tracks and transcript status for a video.
import { NextRequest, NextResponse } from "next/server";
import { getAvailableAudioLanguages } from "@/lib/transcription";
import { getTranscriptLanguagesByKalturaId } from "@/lib/db";
Expand Down
89 changes: 44 additions & 45 deletions app/api/og/meeting/[...slug]/route.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ImageResponse } from "next/og";
import { getTranslations } from "next-intl/server";
// Generates an Open Graph image for a meeting page.
import type { VideoRecord } from "@/lib/db";
import { getVideoByAssetId, getVideoByCitation } from "@/lib/db";
import { symbolFromSlug } from "@/lib/meeting-slug";
import { OgHeader, getOgFonts } from "@/lib/og";
import type { VideoRecord } from "@/lib/db";
import { getTranslations } from "next-intl/server";
import { ImageResponse } from "next/og";

async function resolveVideo(slug: string): Promise<VideoRecord | null> {
if (slug.startsWith("asset/")) {
Expand Down Expand Up @@ -62,66 +63,64 @@ export async function GET(
const metaParts = [category, date].filter(Boolean);

return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
background: "#fff",
color: "#1a1a1a",
// Teams (and iMessage's square thumbnail) crop the OG image with
// object-fit: cover to a near-square box, dropping the outer ~17%
// on each side. The horizontal safe-area padding keeps the header
// + title + meta inside that center band on those clients without
// making the wide-format unfurls look hollow. See app/[locale]/
// opengraph-image.tsx for the matching site-card padding.
padding: "72px 200px",
fontFamily: "Roboto",
}}
>
<OgHeader
brand={t("wordmarkBrand")}
descriptor={t("wordmarkDescriptor")}
badge={t("publicPreview")}
/>
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
background: "#fff",
color: "#1a1a1a",
// Teams (and iMessage's square thumbnail) crop the OG image with
// object-fit: cover to a near-square box, dropping the outer ~17%
// on each side. The horizontal safe-area padding keeps the header
// + title + meta inside that center band on those clients without
// making the wide-format unfurls look hollow. See app/[locale]/
// opengraph-image.tsx for the matching site-card padding.
padding: "72px 200px",
fontFamily: "Roboto",
gap: 32,
paddingBottom: 24,
}}
>
<OgHeader
brand={t("wordmarkBrand")}
descriptor={t("wordmarkDescriptor")}
badge={t("publicPreview")}
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 32,
paddingBottom: 24,
fontSize: titleSize,
fontWeight: 700,
lineHeight: 1.1,
letterSpacing: "-0.01em",
overflow: "hidden",
}}
>
{title}
</div>
{metaParts.length > 0 && (
<div
style={{
display: "flex",
fontSize: titleSize,
fontWeight: 700,
lineHeight: 1.1,
letterSpacing: "-0.01em",
overflow: "hidden",
fontSize: 36,
fontWeight: 400,
color: "#555",
}}
>
{title}
{metaParts.join(" · ")}
</div>
{metaParts.length > 0 && (
<div
style={{
display: "flex",
fontSize: 36,
fontWeight: 400,
color: "#555",
}}
>
{metaParts.join(" · ")}
</div>
)}
</div>
)}
</div>
),
</div>,
{ ...SIZE, fonts },
);
}
1 change: 1 addition & 0 deletions app/api/pv/align/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Aligns a parsed PV document with audio to produce per-turn timestamps.
import { NextRequest, NextResponse } from "next/server";
import { getPVContent, savePVContent } from "@/lib/db";
import { getKalturaAudioUrl } from "@/lib/transcription";
Expand Down
1 change: 1 addition & 0 deletions app/api/pv/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Fetches and parses a UN PV or SR document PDF to structured JSON.
import { NextRequest, NextResponse } from "next/server";
import { getPVContent, savePVContent } from "@/lib/db";
import { fetchPVDocument } from "@/lib/pv-documents";
Expand Down
1 change: 1 addition & 0 deletions app/api/speakers/statements/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Returns paginated statements attributed to a speaker entity.
import { NextRequest, NextResponse } from "next/server";
import { requireExperimental } from "@/lib/auth/require-experimental";
import { apiError } from "@/lib/api-error";
Expand Down
1 change: 1 addition & 0 deletions app/api/subscriptions/feed/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Adds or removes a category feed subscription for the current user.
import { NextRequest, NextResponse } from "next/server";
import { requireUser } from "@/lib/auth/require-user";
import {
Expand Down
1 change: 1 addition & 0 deletions app/api/subscriptions/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Returns the current user's feed and video subscription state.
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth/service";
import {
Expand Down
1 change: 1 addition & 0 deletions app/api/subscriptions/video/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Adds or removes a per-video transcript subscription for the current user.
import { NextRequest, NextResponse } from "next/server";
import { requireUser } from "@/lib/auth/require-user";
import { addVideoSubscription, removeVideoSubscription } from "@/lib/db";
Expand Down
1 change: 1 addition & 0 deletions app/api/transcripts/[id]/analysis/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Runs on-demand proposition analysis on a completed transcript.
import { NextRequest, NextResponse } from "next/server";
import { AzureOpenAI } from "openai";
import { analyzePropositions } from "@/lib/pipeline";
Expand Down
1 change: 1 addition & 0 deletions app/api/transcripts/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Polls transcript pipeline status and returns the result when complete.
import { NextRequest, NextResponse } from "next/server";
import { createHash } from "crypto";
import { pollTranscription } from "@/lib/transcription";
Expand Down
1 change: 1 addition & 0 deletions app/api/transcripts/[id]/words/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Returns word-level timestamps for a completed transcript.
import { NextRequest } from "next/server";
import { getTranscriptByIdForDisplay } from "@/lib/db";
import { apiError } from "@/lib/api-error";
Expand Down
1 change: 1 addition & 0 deletions app/api/transcripts/check/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Checks for an existing transcript by Kaltura ID and language.
import { NextRequest } from "next/server";
import {
getActiveTranscriptByKalturaId,
Expand Down
1 change: 1 addition & 0 deletions app/api/transcripts/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Starts or schedules a new transcription for a video.
import { NextRequest, NextResponse } from "next/server";
import {
getTranscriptByKalturaId,
Expand Down
1 change: 1 addition & 0 deletions app/api/videos/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Paginated video list with transcript availability flags.
import { NextRequest, NextResponse } from "next/server";
import { queryVideos, type VideosQueryParams } from "@/lib/db";
import {
Expand Down
1 change: 1 addition & 0 deletions app/api/waitlist/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Records experimental-features waitlist interest for the current user.
import { NextResponse } from "next/server";
import { requireUser } from "@/lib/auth/require-user";
import { setExperimentalWaitlist } from "@/lib/auth/service";
Expand Down
13 changes: 11 additions & 2 deletions app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ Returns a paginated list of UN meetings matching the given filters. Covers the l
|-----------|------|-------------|
| \`q\` | string | Search meeting titles and metadata (not transcript content). Min 2 characters. |
| \`category\` | string | Filter by meeting category. |
| \`date\` | YYYY-MM-DD | Filter to a specific date. |
| \`date\` | YYYY-MM-DD | Filter to a specific date. Mutually exclusive with \`from\`/\`to\`. |
| \`from\` | YYYY-MM-DD | Inclusive start of a date range. |
| \`to\` | YYYY-MM-DD | Inclusive end of a date range. |
| \`sort\` | enum | \`date_desc\` (default), \`date_asc\`, \`title_asc\`, \`title_desc\` |
| \`offset\`| integer | Pagination offset. Results come in chunks of 250. |
| \`text\` | string (multi) | Filter by available documents: \`transcript\`, \`pv\` (verbatim record), \`sr\` (summary record). |
| \`text\` | string (multi) | Filter by document type: \`transcript\` = has automatic transcript; \`pv\` = has official verbatim record; \`sr\` = has official summary record. Use \`text=transcript\` to exclude meetings with no content. |
| \`xlang\` | \`1\` | Include meetings not yet available in the URL locale (default: hide them). |

### Response shape
Expand Down Expand Up @@ -233,6 +235,13 @@ Closed or confidential meetings are not covered (they are not recorded on Web TV
- **Time window**: search and browse cover the last 365 days, matching the website homepage.
- **Transcript accuracy**: these are automatic speech recognition outputs, not official records. Names, abbreviations, and document symbols may be misheard. Accuracy varies by speaker and microphone quality.
- **Languages**: six UN languages are supported (en, fr, es, ar, zh, ru). Not every meeting has transcripts in all languages — it depends on which audio tracks are available.

---

## Machine-readable spec

OpenAPI 3.0 spec: \`GET /openapi.json\`
Interactive reference: \`/openapi\`
`;

export function GET() {
Expand Down
1 change: 1 addition & 0 deletions app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The locale prefix in the URL (\`/en\`, \`/fr\`, \`/ar\`, \`/zh\`, \`/ru\`, \`/es
- Read transcript (text): \`GET /en/{slug}.txt\` — plain-text transcript with speaker labels, compact for LLM context.
- Read transcript (JSON): \`GET /en/{slug}.json\` — structured JSON with timestamps, speakers, topics, and optional word-level timing.
- [Full API reference](/llms-full.txt): detailed query parameters, response shapes, and known limitations.
- [OpenAPI spec](/openapi.json): machine-readable OpenAPI 3.0 spec (interactive UI at /openapi).

## Meeting URL scheme

Expand Down
32 changes: 32 additions & 0 deletions app/openapi/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Serves the Swagger UI API reference at /openapi.
// Using a plain route handler (not a React page) avoids React strict-mode
// warnings from swagger-ui-react's legacy class components.
export async function GET() {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>API Reference — UN Transcripts</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" />
<style>body { margin: 0; }</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
Comment on lines +11 to +16
<script>
SwaggerUIBundle({
url: "/openapi.json",
dom_id: "#swagger-ui",
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
layout: "BaseLayout",
});
</script>
</body>
</html>`;

return new Response(html, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
Loading
Loading