diff --git a/.agents/skills/relatorio-cidade/SKILL.md b/.agents/skills/relatorio-cidade/SKILL.md index 2e837fed..664b577d 100644 --- a/.agents/skills/relatorio-cidade/SKILL.md +++ b/.agents/skills/relatorio-cidade/SKILL.md @@ -178,7 +178,9 @@ cacheado em `data/relatorios-cidade/.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`. diff --git a/docs/changelog/2026-09-15-c163-falas-minutagem.md b/docs/changelog/2026-09-15-c163-falas-minutagem.md new file mode 100644 index 00000000..7a5cd65f --- /dev/null +++ b/docs/changelog/2026-09-15-c163-falas-minutagem.md @@ -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. diff --git a/scripts/cityReportSnapshot.mjs b/scripts/cityReportSnapshot.mjs index 598be0ed..4dbfdc04 100644 --- a/scripts/cityReportSnapshot.mjs +++ b/scripts/cityReportSnapshot.mjs @@ -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 diff --git a/scripts/lib/cityReportBlocks.mjs b/scripts/lib/cityReportBlocks.mjs index 2eb6bff0..84e97f76 100644 --- a/scripts/lib/cityReportBlocks.mjs +++ b/scripts/lib/cityReportBlocks.mjs @@ -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: [ diff --git a/tests/unit/cityReportBlocks.unit.spec.ts b/tests/unit/cityReportBlocks.unit.spec.ts index 903cbc73..ecc9634c 100644 --- a/tests/unit/cityReportBlocks.unit.spec.ts +++ b/tests/unit/cityReportBlocks.unit.spec.ts @@ -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')