feat(C163): link do vídeo do acervo na minutagem do trecho

This commit is contained in:
Francisco Solla 2026-09-15 09:48:56 -03:00
parent 392cfa51d4
commit 3e7f750141
5 changed files with 26 additions and 3 deletions

View file

@ -178,7 +178,9 @@ cacheado em `data/relatorios-cidade/<base>.emendas.json` para replay.
econômica (pesquisa)` · `9. Transporte e conexões (pesquisa)` · `10. Acervo
de falas` — cada fala com "O que é" (sumário oficial) e "Menção ao município"
(passagem que cita a cidade ou, se o nome não aparece nos trechos, a marcação
do acervo com o nº de municípios), mais **Vídeo** (YouTube/VOD da Câmara) e
do acervo com o nº de municípios), mais **Vídeo** (YouTube já na minutagem
do trecho — `youtubeOffsetSeconds` = época do trecho − início da sessão;
fallback para o VOD da Câmara, que é o clipe do próprio trecho) e
**Transcrição** (PDF do Diário) quando existirem · `11. Notícias e imprensa` ·
`12. Panorama regional` · `13. Abordagem sugerida (personas)` · `14. Fontes e
limites`.

View file

@ -0,0 +1 @@
**C163 — vídeo do acervo já na minutagem do trecho (2026-09-15):** o link do YouTube no acervo de falas agora abre no momento em que o deputado fala. O extrator passou a projetar `youtubeOffsetSeconds` usando o dono `excerptOffsetSeconds` (`src/lib/speechVod.ts` — época do trecho menos o início da sessão, ambos em hora de Brasília) e o builder acrescenta `&t=Ns` (ou `?t=Ns`) à URL do YouTube; quando não há offset conhecido, o link fica sem timestamp, e o VOD (clipe do próprio trecho) segue como fallback sem timestamp. Na fala de Itamaraju (641): offset de 645s (10min45s) — `youtube.com/watch?v=2cX_gKkJH7Q&t=645s`. Unit 3118/3118, `tsc`/eslint verdes; snapshot reextraído e PDF regerado.

View file

@ -14,6 +14,7 @@ import { FEDERAL_DEPUTY_OFFICE, HISTORICAL_SERIES_YEARS } from '../src/lib/elect
import { getMunicipalityCatalogEntry } from '../src/lib/municipalityCatalog.ts'
import { getMunicipalityVoteRank } from '../src/lib/municipalityVoteRank.ts'
import { normalizeForSearch } from '../src/lib/speechSearch.ts'
import { excerptOffsetSeconds } from '../src/lib/speechVod.ts'
import { campaignRoleLabels } from '../src/utilities/campaignUserProfile.ts'
import { loadMunicipalityDossierData } from '../src/utilities/municipality/municipalityDossierData.ts'
import {
@ -65,6 +66,8 @@ const loadSpeeches = async (payload, actor, municipalityID, municipalityName) =>
officialTextUrl: true,
youtubeUrl: true,
vodPlaybackUrl: true,
excerptTMs: true,
eventStartAt: true,
mentionedMunicipalities: true,
},
user: actor,
@ -92,6 +95,8 @@ const loadSpeeches = async (payload, actor, municipalityID, municipalityName) =>
officialTextUrl: doc.officialTextUrl ?? null,
youtubeUrl: doc.youtubeUrl ?? null,
vodPlaybackUrl: doc.vodPlaybackUrl ?? null,
/** Offset of the excerpt inside the session video (YouTube start). */
youtubeOffsetSeconds: excerptOffsetSeconds(doc.excerptTMs, doc.eventStartAt),
mentionExcerpt: mentionSegment?.text ?? null,
mentionedMunicipalityCount: Array.isArray(doc.mentionedMunicipalities)
? doc.mentionedMunicipalities.length

View file

@ -852,6 +852,18 @@ const buildTransportSection = ({ research }) =>
items: research.transport ?? [],
})
/**
* YouTube link starts where the excerpt sits in the session video
* (`youtubeOffsetSeconds`, from the Câmara excerpt epoch − session start).
* The VOD clip is the excerpt itself, so it needs no timestamp.
*/
const withYoutubeTimestamp = (url, offsetSeconds) => {
if (!url) return null
if (!Number.isFinite(offsetSeconds) || offsetSeconds <= 0) return url
const separator = url.includes('?') ? '&' : '?'
return `${url}${separator}t=${Math.floor(offsetSeconds)}s`
}
const buildSpeechesSection = ({ snapshot }) => {
const { speeches, municipality } = snapshot
if (!speeches.rows.length) {
@ -889,7 +901,9 @@ const buildSpeechesSection = ({ snapshot }) => {
: row.mentionedMunicipalityCount
? `Marcada no acervo — nome não localizado nos trechos (a fala cita ${formatInteger(row.mentionedMunicipalityCount)} municípios).`
: 'Marcada no acervo — trecho não localizado.',
video: row.youtubeUrl ?? row.vodPlaybackUrl ?? '—',
video: row.youtubeUrl
? withYoutubeTimestamp(row.youtubeUrl, row.youtubeOffsetSeconds)
: (row.vodPlaybackUrl ?? '—'),
transcript: row.officialTextUrl ?? '—',
})),
note: [

View file

@ -278,6 +278,7 @@ const snapshot = {
officialTextUrl: 'https://camara.test/discurso',
youtubeUrl: 'https://youtube.test/video',
vodPlaybackUrl: 'https://vod.test/video.mp4',
youtubeOffsetSeconds: 125,
mentionExcerpt: 'Trecho da fala que cita Feira de Santana',
mentionedMunicipalityCount: 3,
},
@ -444,7 +445,7 @@ describe('buildCityReport', () => {
expect(falas.columns!.find((column) => column.key === 'mention')!.width).toBe(27)
expect(rows[0].description).toBe('Resumo da fala')
expect(rows[0].mention).toContain('cita Feira de Santana')
expect(rows[0].video).toBe('https://youtube.test/video')
expect(rows[0].video).toBe('https://youtube.test/video?t=125s')
expect(rows[0].transcript).toBe('https://camara.test/discurso')
expect(rows[1].description).toBe('Saúde indígena na Bahia')
expect(rows[1].mention).toContain('nome não localizado nos trechos')