teqo-backup/docs/changelog/2026-09-15-c167.md
Francisco Solla 352801853f feat(C167): corta o trecho [início,fim] do acervo e publica o arquivo numa página compartilhável
Seleção do C166 ganha "Cortar vídeo": título/descrição por IA (editáveis, com fallback determinístico), job assíncrono com ffmpeg por re-encode exato e polling dos quatro passos, e página pública unlisted /corte/<id> (noindex, OG, crédito CC BY) servindo o mesmo MP4. Collection speechCut com access canReadSpeechCut (advisor/leader negados) e kill switch por status; ffmpeg entra só no runner da imagem com FFMPEG_PATH para teste.
2026-09-15 10:24:36 -03:00

3 KiB

C167 — Acervo: cortar o trecho [início,fim] e publicar o arquivo numa página compartilhável (2026-09-15): da seleção do C166, o botão "Cortar vídeo" abre o formulário com título e descrição pré-preenchidos (IA DeepSeek sobre a janela de transcrição do trecho, editáveis; IA indisponível cai no determinístico Trecho de <tipo> — <data> + sumário oficial, sem bloquear) e "Cortar e publicar" cria o corte e acompanha quatro passos honestos ("Localizando o trecho na Câmara" → "Cortando o trecho" → "Gerando título e descrição" → "Publicando a página do corte") até o card de sucesso com link, Copiar, WhatsApp e Baixar MP4. O arquivo é o MP4 EXATO [início,fim]: o job (speechCutJob) re-resolve o VOD da Câmara pelo C162, baixa, corta com ffmpeg por re-encode (-ss antes do -i, -t = duração, x264/aac/+faststart — nunca -c copy, que aproximaria keyframe), grava em media e publica a row; ffmpeg entra só no estágio runner do Dockerfile (apk add ffmpeg, com FFMPEG_PATH para fake em teste). O request não espera o render: a row nasce processing, o trabalho roda em after() e o cliente faz poll (~1,5 s) do passo; um reaper lazy marca processing parado há 15 min como failed (restart/deploy) e o retry reusa a MESMA row — falhou, nada publica. Nova collection speechCut (migration add_speech_cut; speech opcional para o FK set null sobreviver ao delete da fala; status processing|published|unpublished|failed é o kill switch; media/publishedAt/error internos; createdBy por systemStampedActorField) com access canReadSpeechCut: leitor do acervo vê tudo, o público lê só published no where (REST/GraphQL protegidos) e advisor/leader são negados fail-closed nas rotas e na página. A página pública /corte/<id> (unlisted, robots noindex, sem transcrição/internals) toca o MESMO arquivo via proxy Range do Payload, baixa com download, mostra contexto mínimo (tipo/data/duração + sessão no YouTube), crédito CC BY 4.0 e OG com a capa do YouTube (senão a imagem global do site); despublicado e id inexistente respondem o mesmo 404 amigável, sem revelar se o corte existiu, com bust de cache no afterChange do kill switch. O transporte reusa o padrão das rotas JSON do /campanha (campaignJsonMutationRoute, same-origin + zod) em cortar, cortar/status e cortar/sugestao. Testes: unit do módulo puro (args do ffmpeg, fallback, passos, view model, share), RTL do diálogo (prefill/IA, dirty flag, poll → publicado, falha → retry sem duplicar), int (matriz de access, create/retry/dedupe in-flight/reaper, job feliz com fake ffmpeg e bloco real com ffprobe sob skipIf) e e2e browserless campaignSpeechCut (página pública 200/404/noindex, gate das rotas) — spec novo entra no set curado (diff high-risk por migration) com manifest atualizado. Sem Consent/segundo cadastro, sem upload externo, sem editor/legenda/fila (C168 é a biblioteca).