mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
This commit is contained in:
parent
e91dea637f
commit
af6fa698db
4 changed files with 1752 additions and 0 deletions
820
docs/plans/acervo-gravacoes-enviadas-ui-design.html
Normal file
820
docs/plans/acervo-gravacoes-enviadas-ui-design.html
Normal file
|
|
@ -0,0 +1,820 @@
|
|||
<!doctype html>
|
||||
<html lang="pt-BR" data-theme="campaign">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<title>Design — Gravações enviadas no acervo</title>
|
||||
<style type="text/tailwindcss">
|
||||
@theme {
|
||||
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
--color-background: #ffffff;
|
||||
--color-foreground: #1c1917;
|
||||
--color-card: #ffffff;
|
||||
--color-primary: #c51414;
|
||||
--color-primary-foreground: #ffffff;
|
||||
--color-secondary: #f5f5f4;
|
||||
--color-secondary-foreground: #292524;
|
||||
--color-muted: #f4f4f5;
|
||||
--color-muted-foreground: #6b7280;
|
||||
--color-border: rgb(28 25 23 / 12%);
|
||||
--color-input: #d6d3d1;
|
||||
--color-destructive: #b42318;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.icon-sm {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
min-height: 2.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
transition: 150ms ease;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #c51414;
|
||||
color: white;
|
||||
border: 1px solid #c51414;
|
||||
}
|
||||
.btn-outline {
|
||||
background: white;
|
||||
color: #292524;
|
||||
border: 1px solid #d6d3d1;
|
||||
}
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: #292524;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.field {
|
||||
min-height: 2.75rem;
|
||||
width: 100%;
|
||||
border: 1px solid #d6d3d1;
|
||||
border-radius: 0.5rem;
|
||||
background: white;
|
||||
padding: 0.625rem 0.75rem;
|
||||
color: #1c1917;
|
||||
outline: none;
|
||||
}
|
||||
.field:focus {
|
||||
border-color: #c51414;
|
||||
box-shadow: 0 0 0 3px rgb(197 20 20 / 18%);
|
||||
}
|
||||
.scene-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #57534e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.scene-label::after {
|
||||
content: '';
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
background: #d6d3d1;
|
||||
}
|
||||
.desktop-frame {
|
||||
width: 1280px;
|
||||
min-height: 720px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d6d3d1;
|
||||
border-radius: 14px;
|
||||
background: white;
|
||||
box-shadow: 0 18px 45px rgb(28 25 23 / 10%);
|
||||
}
|
||||
.mobile-frame {
|
||||
width: 390px;
|
||||
min-height: 760px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d6d3d1;
|
||||
border-radius: 28px;
|
||||
background: white;
|
||||
box-shadow: 0 18px 45px rgb(28 25 23 / 12%);
|
||||
}
|
||||
.status {
|
||||
display: inline-flex;
|
||||
min-height: 1.25rem;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
border-radius: 9999px;
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.status-ready {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
.status-work {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
.status-failed {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
.status-upload {
|
||||
background: #eef0f3;
|
||||
color: #46505c;
|
||||
}
|
||||
.seg {
|
||||
display: grid;
|
||||
grid-template-columns: 3.25rem 1fr;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
.seg:hover,
|
||||
.seg-active {
|
||||
background: #f4f4f5;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-stone-100 text-foreground antialiased">
|
||||
<svg aria-hidden="true" class="absolute size-0 overflow-hidden">
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 3v12m0-12 4 4m-4-4L8 7" />
|
||||
<path d="M4 15v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4" />
|
||||
</symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="m6 4 14 8-14 8z" />
|
||||
</symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 3v12m0 0 4-4m-4 4-4-4" />
|
||||
<path d="M5 21h14" />
|
||||
</symbol>
|
||||
<symbol id="i-video" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<rect x="3" y="5" width="14" height="14" rx="2" />
|
||||
<path d="m17 10 4-2v8l-4-2z" />
|
||||
</symbol>
|
||||
<symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<path d="M14 2v6h6" />
|
||||
</symbol>
|
||||
<symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M20 7h-5V2" />
|
||||
<path d="M20 7a9 9 0 1 0 1 8" />
|
||||
</symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="m18 6-12 12M6 6l12 12" />
|
||||
</symbol>
|
||||
<symbol id="i-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</symbol>
|
||||
<symbol id="i-back" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</symbol>
|
||||
<symbol id="i-library" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="m16 6 4 14M12 6v14M8 8v12M4 4v16" />
|
||||
</symbol>
|
||||
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<main class="mx-auto w-max max-w-none px-10 py-10">
|
||||
<header class="mb-8 max-w-3xl">
|
||||
<p class="text-sm font-semibold text-primary">Teqo · acervo de comunicação</p>
|
||||
<h1 class="mt-1 text-2xl font-semibold tracking-tight">Gravações enviadas no acervo</h1>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
Referência hi-fi para enviar, acompanhar, buscar e consultar gravações próprias. A
|
||||
transcrição é somente leitura.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- CENA 1 -->
|
||||
<section class="mb-12" aria-labelledby="scene-1">
|
||||
<h2 id="scene-1" class="scene-label">Cena 1 · Desktop — acervo com alternador de fonte</h2>
|
||||
<div class="desktop-frame flex">
|
||||
<aside class="flex w-[220px] shrink-0 flex-col border-r border-border bg-stone-50 p-3">
|
||||
<div class="flex h-12 items-center gap-2 px-2 font-semibold">
|
||||
<span class="grid size-8 place-items-center rounded-lg bg-primary text-sm text-white"
|
||||
>T</span
|
||||
>
|
||||
Campanha
|
||||
</div>
|
||||
<nav class="mt-3 space-y-1 text-sm">
|
||||
<div class="rounded-md px-3 py-2 text-muted-foreground">Início</div>
|
||||
<div class="rounded-md px-3 py-2 text-muted-foreground">Municípios</div>
|
||||
<div class="rounded-md px-3 py-2 text-muted-foreground">Pessoas</div>
|
||||
<div
|
||||
class="rounded-md bg-white px-3 py-2 font-medium text-primary ring-1 ring-border"
|
||||
>
|
||||
Comunicação
|
||||
</div>
|
||||
<div class="ml-5 border-l border-border py-1 pl-3">
|
||||
<div
|
||||
class="rounded-md bg-white px-3 py-2 font-medium text-primary ring-1 ring-border"
|
||||
>
|
||||
Acervo
|
||||
</div>
|
||||
<div class="px-3 py-2 text-muted-foreground">Cortes</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="mt-auto border-t border-border px-2 pt-3 text-xs text-muted-foreground">
|
||||
Assessor de Comunicação
|
||||
</div>
|
||||
</aside>
|
||||
<div class="min-w-0 flex-1">
|
||||
<header class="flex min-h-11 items-center gap-3 border-b border-border px-4">
|
||||
<span class="text-muted-foreground">☰</span>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold">Acervo</p>
|
||||
<p class="text-xs text-muted-foreground">Comunicação</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-6">
|
||||
<div class="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold tracking-tight">Acervo de falas</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Busque nas falas da Câmara ou nas gravações da equipe.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-primary">
|
||||
<svg class="icon"><use href="#i-upload" /></svg>Enviar gravação
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mt-5 inline-flex rounded-lg bg-secondary p-1 text-sm"
|
||||
role="tablist"
|
||||
aria-label="Fonte do acervo"
|
||||
>
|
||||
<button class="rounded-md px-3 py-2 text-muted-foreground" data-source-tab="camara">
|
||||
Falas da Câmara
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md bg-white px-3 py-2 font-medium text-foreground shadow-sm ring-1 ring-border"
|
||||
data-source-tab="enviadas"
|
||||
>
|
||||
Gravações enviadas
|
||||
</button>
|
||||
</div>
|
||||
<label class="mt-5 block text-sm font-medium" for="search-1"
|
||||
>Buscar nas gravações enviadas</label
|
||||
>
|
||||
<div
|
||||
class="mt-1.5 flex min-h-11 items-center gap-2 rounded-lg border border-input bg-white px-3 shadow-xs"
|
||||
>
|
||||
<svg class="icon text-muted-foreground"><use href="#i-search" /></svg
|
||||
><input
|
||||
id="search-1"
|
||||
class="min-w-0 flex-1 border-0 bg-transparent text-sm outline-none"
|
||||
placeholder="Busque por palavra ou trecho da transcrição…"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-5 space-y-3">
|
||||
<article
|
||||
class="grid grid-cols-[112px_minmax(0,1fr)_auto] gap-4 rounded-xl border border-border bg-card p-4"
|
||||
>
|
||||
<div
|
||||
class="grid aspect-video place-items-center rounded-lg bg-stone-200 text-stone-500"
|
||||
>
|
||||
<svg class="h-7 w-7"><use href="#i-video" /></svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="truncate text-sm font-semibold">Título da gravação</h4>
|
||||
<span class="status status-ready">Pronto</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
<p class="mt-2 line-clamp-2 text-sm leading-5 text-foreground/90">
|
||||
Texto transcrito da gravação aparece aqui quando corresponde à busca.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-outline self-center">
|
||||
Abrir<svg class="icon-sm"><use href="#i-chevron" /></svg>
|
||||
</button>
|
||||
</article>
|
||||
<article
|
||||
class="grid grid-cols-[112px_minmax(0,1fr)_auto] gap-4 rounded-xl border border-border bg-card p-4"
|
||||
>
|
||||
<div
|
||||
class="grid aspect-video place-items-center rounded-lg bg-stone-200 text-stone-500"
|
||||
>
|
||||
<svg class="h-7 w-7"><use href="#i-video" /></svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="truncate text-sm font-semibold">Outra gravação enviada</h4>
|
||||
<span class="status status-work">Processando</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
Transcrevendo a gravação. Ela ainda não aparece na busca.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-outline self-center">
|
||||
Abrir<svg class="icon-sm"><use href="#i-chevron" /></svg>
|
||||
</button>
|
||||
</article>
|
||||
<article
|
||||
class="grid grid-cols-[112px_minmax(0,1fr)_auto] gap-4 rounded-xl border border-border bg-card p-4"
|
||||
>
|
||||
<div
|
||||
class="grid aspect-video place-items-center rounded-lg bg-stone-200 text-stone-500"
|
||||
>
|
||||
<svg class="h-7 w-7"><use href="#i-video" /></svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="truncate text-sm font-semibold">
|
||||
Gravação com falha na transcrição
|
||||
</h4>
|
||||
<span class="status status-failed">Falhou</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
O arquivo foi preservado. Reprocesse a transcrição no detalhe.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-outline self-center">
|
||||
Abrir<svg class="icon-sm"><use href="#i-chevron" /></svg>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 2 -->
|
||||
<section class="mb-12" aria-labelledby="scene-2">
|
||||
<h2 id="scene-2" class="scene-label">
|
||||
Cena 2 · Desktop — estados de processamento na lista e no detalhe
|
||||
</h2>
|
||||
<div class="desktop-frame p-8">
|
||||
<div class="mx-auto max-w-5xl">
|
||||
<div class="flex items-end justify-between">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">Estados da gravação</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
O arquivo permanece visível desde o envio.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">Gravações enviadas</span>
|
||||
</div>
|
||||
<div class="mt-6 grid grid-cols-2 gap-4">
|
||||
<article class="rounded-xl border border-border p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<span class="status status-upload">Enviando</span>
|
||||
<h4 class="mt-3 text-sm font-semibold">Título da gravação</h4>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Arquivo sendo enviado · ainda não pesquisável
|
||||
</p>
|
||||
</div>
|
||||
<svg class="h-5 w-5 text-muted-foreground"><use href="#i-upload" /></svg>
|
||||
</div>
|
||||
<div class="mt-5 h-2 overflow-hidden rounded-full bg-muted">
|
||||
<div class="h-full w-[58%] rounded-full bg-primary"></div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-muted-foreground">Enviando arquivo…</p>
|
||||
</article>
|
||||
<article class="rounded-xl border border-border p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<span class="status status-work">Processando</span>
|
||||
<h4 class="mt-3 text-sm font-semibold">Título da gravação</h4>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Transcrevendo · ainda não pesquisável
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="size-5 animate-spin rounded-full border-2 border-stone-300 border-t-primary motion-reduce:animate-none"
|
||||
></span>
|
||||
</div>
|
||||
<div class="mt-5 rounded-lg bg-muted/70 p-3 text-xs text-muted-foreground">
|
||||
A transcrição ficará disponível neste detalhe quando o processamento terminar.
|
||||
</div>
|
||||
</article>
|
||||
<article class="rounded-xl border border-destructive/30 p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<span class="status status-failed">Falhou</span>
|
||||
<h4 class="mt-3 text-sm font-semibold">Título da gravação</h4>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
A transcrição não foi concluída. O arquivo foi preservado.
|
||||
</p>
|
||||
</div>
|
||||
<svg class="h-5 w-5 text-destructive"><use href="#i-file" /></svg>
|
||||
</div>
|
||||
<button class="btn btn-outline mt-5">
|
||||
<svg class="icon-sm"><use href="#i-refresh" /></svg>Reprocessar transcrição
|
||||
</button>
|
||||
</article>
|
||||
<article class="rounded-xl border border-border p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<span class="status status-ready">Pronto</span>
|
||||
<h4 class="mt-3 text-sm font-semibold">Título da gravação</h4>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Transcrição disponível e pesquisável
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="grid size-6 place-items-center rounded-full bg-green-100 text-sm text-green-700"
|
||||
>✓</span
|
||||
>
|
||||
</div>
|
||||
<button class="btn btn-outline mt-5">
|
||||
<svg class="icon-sm"><use href="#i-play" /></svg>Abrir gravação
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 3 -->
|
||||
<section class="mb-12" aria-labelledby="scene-3">
|
||||
<h2 id="scene-3" class="scene-label">Cena 3 · Desktop — detalhe da gravação</h2>
|
||||
<div class="desktop-frame">
|
||||
<header class="flex min-h-11 items-center gap-3 border-b border-border px-4">
|
||||
<span class="text-muted-foreground">☰</span>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Gravação enviada</p>
|
||||
<p class="text-xs text-muted-foreground">Acervo</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-6">
|
||||
<button class="btn btn-ghost -ml-2">
|
||||
<svg class="icon"><use href="#i-back" /></svg>Voltar ao acervo
|
||||
</button>
|
||||
<div class="mt-4 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Título da gravação</h3>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
</div>
|
||||
<span class="status status-ready">Pronto</span>
|
||||
</div>
|
||||
<div class="mt-5 grid grid-cols-[minmax(0,1.35fr)_minmax(340px,.65fr)] gap-6">
|
||||
<div>
|
||||
<div
|
||||
class="relative grid aspect-video place-items-center overflow-hidden rounded-lg border border-black/30 bg-stone-900"
|
||||
>
|
||||
<span
|
||||
class="absolute inset-0 bg-[linear-gradient(145deg,#292524_0%,#0c0a09_100%)]"
|
||||
></span
|
||||
><button
|
||||
class="relative grid size-16 place-items-center rounded-full bg-white text-stone-950 shadow-lg"
|
||||
>
|
||||
<svg class="h-7 w-7 translate-x-0.5"><use href="#i-play" /></svg>
|
||||
</button>
|
||||
<div
|
||||
class="absolute inset-x-4 bottom-3 flex items-center gap-3 text-xs text-white"
|
||||
>
|
||||
<span>00:00</span>
|
||||
<div class="h-1 flex-1 rounded-full bg-white/30">
|
||||
<div class="h-full w-[18%] rounded-full bg-white"></div>
|
||||
</div>
|
||||
<span>—:—:—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<button class="btn btn-primary">
|
||||
<svg class="icon"><use href="#i-download" /></svg>Baixar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<section aria-label="Transcrição">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-xs font-medium text-muted-foreground">TRANSCRIÇÃO</h4>
|
||||
<span class="text-xs text-muted-foreground">Clique para posicionar</span>
|
||||
</div>
|
||||
<ol class="mt-2 space-y-0.5">
|
||||
<li>
|
||||
<button class="seg seg-active">
|
||||
<span class="pt-0.5 text-xs tabular-nums text-primary">00:00</span
|
||||
><span class="text-sm leading-relaxed"
|
||||
>Texto transcrito do primeiro segmento da gravação aparece aqui.</span
|
||||
>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="seg">
|
||||
<span class="pt-0.5 text-xs tabular-nums text-muted-foreground">00:18</span
|
||||
><span class="text-sm leading-relaxed"
|
||||
>O próximo segmento mantém a leitura contínua e posiciona o player quando
|
||||
selecionado.</span
|
||||
>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="seg">
|
||||
<span class="pt-0.5 text-xs tabular-nums text-muted-foreground">00:42</span
|
||||
><span class="text-sm leading-relaxed"
|
||||
>A transcrição é somente leitura nesta versão do acervo.</span
|
||||
>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="seg">
|
||||
<span class="pt-0.5 text-xs tabular-nums text-muted-foreground">01:06</span
|
||||
><span class="text-sm leading-relaxed"
|
||||
>Mais um trecho transcrito continua disponível para busca.</span
|
||||
>
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 4 -->
|
||||
<section class="mb-12" aria-labelledby="scene-4">
|
||||
<h2 id="scene-4" class="scene-label">Cena 4 · Desktop — envio de gravação</h2>
|
||||
<div class="desktop-frame relative grid place-items-center bg-stone-200/70 p-8">
|
||||
<div class="absolute inset-0 bg-stone-950/25"></div>
|
||||
<section
|
||||
class="relative w-full max-w-lg rounded-xl border border-border bg-white p-6 shadow-2xl"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="upload-title"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 id="upload-title" class="text-base font-semibold">Enviar gravação</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Adicione um arquivo local ao acervo.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="grid size-8 place-items-center rounded-md hover:bg-muted"
|
||||
aria-label="Fechar"
|
||||
>
|
||||
<svg class="icon"><use href="#i-x" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-5 space-y-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium">Arquivo de vídeo</label
|
||||
><button
|
||||
class="mt-1.5 flex min-h-28 w-full flex-col items-center justify-center rounded-lg border border-dashed border-input bg-stone-50 px-4 text-center"
|
||||
>
|
||||
<svg class="h-6 w-6 text-muted-foreground"><use href="#i-upload" /></svg
|
||||
><span class="mt-2 text-sm font-medium">Selecionar arquivo local</span
|
||||
><span class="mt-1 text-xs text-muted-foreground"
|
||||
>O limite será informado antes do envio.</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium" for="title-upload"
|
||||
>Título <span class="text-destructive">*</span></label
|
||||
><input
|
||||
id="title-upload"
|
||||
class="field mt-1.5 text-sm"
|
||||
placeholder="Título da gravação"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium" for="date-upload">Data da gravação</label
|
||||
><input
|
||||
id="date-upload"
|
||||
class="field mt-1.5 text-sm text-muted-foreground"
|
||||
placeholder="dd/mm/aaaa"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<button class="btn btn-outline">Cancelar</button
|
||||
><button class="btn btn-primary">
|
||||
<svg class="icon"><use href="#i-upload" /></svg>Enviar
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 5 -->
|
||||
<section class="mb-12" aria-labelledby="scene-5">
|
||||
<h2 id="scene-5" class="scene-label">Cena 5 · Desktop — vazio</h2>
|
||||
<div class="desktop-frame p-8">
|
||||
<div class="mx-auto max-w-5xl">
|
||||
<div class="inline-flex rounded-lg bg-secondary p-1 text-sm">
|
||||
<span class="rounded-md px-3 py-2 text-muted-foreground">Falas da Câmara</span
|
||||
><span class="rounded-md bg-white px-3 py-2 font-medium shadow-sm ring-1 ring-border"
|
||||
>Gravações enviadas</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="mt-5 flex min-h-72 flex-col items-center justify-center rounded-xl border border-border px-6 text-center"
|
||||
>
|
||||
<span
|
||||
class="grid size-12 place-items-center rounded-lg bg-muted text-muted-foreground"
|
||||
><svg class="h-6 w-6"><use href="#i-video" /></svg
|
||||
></span>
|
||||
<h3 class="mt-4 text-base font-semibold">Nenhuma gravação enviada ainda</h3>
|
||||
<p class="mt-1 max-w-md text-sm text-muted-foreground">
|
||||
Envie um arquivo de vídeo para acompanhar a transcrição e pesquisar o conteúdo no
|
||||
acervo.
|
||||
</p>
|
||||
<button class="btn btn-primary mt-5">
|
||||
<svg class="icon"><use href="#i-upload" /></svg>Enviar gravação
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 6 -->
|
||||
<section aria-labelledby="scene-6">
|
||||
<h2 id="scene-6" class="scene-label">
|
||||
Cena 6 · Mobile (390 px) — lista e detalhe com transcrição
|
||||
</h2>
|
||||
<div class="flex items-start gap-8">
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-medium text-muted-foreground">Lista de gravações</p>
|
||||
<div class="mobile-frame relative pb-20">
|
||||
<header class="flex min-h-14 items-center bg-primary px-4 text-white">
|
||||
<h3 class="text-sm font-semibold">Acervo</h3>
|
||||
<button class="btn ml-auto border border-white/30 bg-white/10 text-white">
|
||||
<svg class="icon"><use href="#i-upload" /></svg>Enviar
|
||||
</button>
|
||||
</header>
|
||||
<div class="border-b border-border bg-white px-4 pt-3 pb-2">
|
||||
<div class="grid grid-cols-2 rounded-lg bg-secondary p-1 text-xs">
|
||||
<button class="rounded-md px-2 py-2 text-muted-foreground">Falas da Câmara</button
|
||||
><button
|
||||
class="rounded-md bg-white px-2 py-2 font-medium shadow-sm ring-1 ring-border"
|
||||
>
|
||||
Gravações enviadas
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 flex min-h-10 items-center gap-2 rounded-lg px-2">
|
||||
<svg class="icon text-muted-foreground"><use href="#i-search" /></svg
|
||||
><span class="text-sm text-muted-foreground">Buscar nas gravações…</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3 p-4">
|
||||
<article class="rounded-xl border border-border p-4">
|
||||
<div class="aspect-video rounded-lg bg-stone-200"></div>
|
||||
<div class="mt-3 flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold">Título da gravação</h4>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
</div>
|
||||
<span class="status status-ready">Pronto</span>
|
||||
</div>
|
||||
<p class="mt-3 text-sm leading-5">
|
||||
Texto transcrito correspondente à busca aparece aqui.
|
||||
</p>
|
||||
<button class="btn btn-outline mt-3 w-full">
|
||||
Abrir gravação<svg class="icon-sm"><use href="#i-chevron" /></svg>
|
||||
</button>
|
||||
</article>
|
||||
<article class="rounded-xl border border-border p-4">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold">Outra gravação</h4>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
</div>
|
||||
<span class="status status-work">Processando</span>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-muted-foreground">
|
||||
Transcrevendo. Ainda não pesquisável.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<nav
|
||||
class="absolute inset-x-0 bottom-0 grid h-16 grid-cols-5 border-t border-border bg-white pt-2 text-[10px] text-muted-foreground"
|
||||
>
|
||||
<span class="text-center">Início</span><span class="text-center">Pessoas</span
|
||||
><span class="text-center text-primary">Comunicação</span
|
||||
><span class="text-center">Agenda</span><span class="text-center">Mais</span>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-medium text-muted-foreground">Detalhe</p>
|
||||
<div class="mobile-frame relative pb-20">
|
||||
<header class="flex min-h-14 items-center gap-3 bg-primary px-4 text-white">
|
||||
<svg class="icon"><use href="#i-back" /></svg>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold">Gravação enviada</h3>
|
||||
<p class="text-xs text-white/75">Acervo</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 class="text-base font-semibold">Título da gravação</h4>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
</div>
|
||||
<span class="status status-ready">Pronto</span>
|
||||
</div>
|
||||
<div
|
||||
class="relative mt-4 grid aspect-video place-items-center rounded-lg bg-stone-900"
|
||||
>
|
||||
<button class="grid size-12 place-items-center rounded-full bg-white">
|
||||
<svg class="h-5 w-5 translate-x-0.5"><use href="#i-play" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary mt-3 w-full">
|
||||
<svg class="icon"><use href="#i-download" /></svg>Baixar
|
||||
</button>
|
||||
<section class="mt-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h5 class="text-xs font-medium text-muted-foreground">TRANSCRIÇÃO</h5>
|
||||
<span class="text-[11px] text-muted-foreground">Toque para posicionar</span>
|
||||
</div>
|
||||
<ol class="mt-2 space-y-1">
|
||||
<li>
|
||||
<button class="seg seg-active">
|
||||
<span class="text-xs text-primary">00:00</span
|
||||
><span class="text-sm leading-5"
|
||||
>Texto transcrito do primeiro segmento.</span
|
||||
>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="seg">
|
||||
<span class="text-xs text-muted-foreground">00:18</span
|
||||
><span class="text-sm leading-5"
|
||||
>O próximo segmento continua a leitura da gravação.</span
|
||||
>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="seg">
|
||||
<span class="text-xs text-muted-foreground">00:42</span
|
||||
><span class="text-sm leading-5"
|
||||
>A transcrição permanece somente leitura.</span
|
||||
>
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
<nav
|
||||
class="absolute inset-x-0 bottom-0 grid h-16 grid-cols-5 border-t border-border bg-white pt-2 text-[10px] text-muted-foreground"
|
||||
>
|
||||
<span class="text-center">Início</span><span class="text-center">Pessoas</span
|
||||
><span class="text-center text-primary">Comunicação</span
|
||||
><span class="text-center">Agenda</span><span class="text-center">Mais</span>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('[data-source-tab]').forEach((tab) => {
|
||||
tab.addEventListener('click', () => {
|
||||
document
|
||||
.querySelectorAll('[data-source-tab]')
|
||||
.forEach((item) => (item.className = 'rounded-md px-3 py-2 text-muted-foreground'))
|
||||
tab.className =
|
||||
'rounded-md bg-white px-3 py-2 font-medium text-foreground shadow-sm ring-1 ring-border'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
116
docs/plans/acervo-gravacoes-enviadas.md
Normal file
116
docs/plans/acervo-gravacoes-enviadas.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Gravações enviadas no acervo de comunicação
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-09-18
|
||||
Issue: #1166
|
||||
Priority: P2
|
||||
Impeccable: C — fluxo novo na vertical `/campanha/comunicacao/acervo` (upload + nova fonte + detalhe)
|
||||
Design UI: docs/plans/acervo-gravacoes-enviadas-ui-design.html
|
||||
Appetite: ~3–4 dias eng; um outcome verificável — a assessoria sobe uma plenária/debate e a gravação fica pesquisável no acervo, com player e transcrição
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
O acervo de falas só tem o que a Câmara publica. Mas a comunicação do mandato também grava plenárias, debates e material próprio que hoje fica em pastas e celulares — sem transcrição, sem busca e sem lugar comum. Quem precisa de um trecho continua garimpando vídeo no olho.
|
||||
|
||||
Este item permite que a assessoria envie um arquivo de gravação e o transforme numa fonte pesquisável dentro do acervo: transcrição com timestamps ao longo de toda a gravação, busca textual que alcança a fala de qualquer pessoa presente (não só o Solla — requisito, não efeito colateral), player que abre no trecho e download do arquivo. É o primeiro passo da linha; separar/rotular quem falou é o C200.
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** assessoria de comunicação (`communicator`), na mesa, lidando com arquivos longos (horas) de plenárias e debates; `coordinator`/`candidate` usam o mesmo acervo. Busca e leitura também acontecem em campo.
|
||||
- **Job principal:** guardar uma gravação própria no acervo e, depois, achar nela a fala/trecho que embasa a peça — de qualquer pessoa que falou.
|
||||
- **Fluxo desejado:** abre o acervo → alterna para "Gravações enviadas" → "Enviar gravação" (arquivo local, título e data) → acompanha o estado → com "Pronto", busca por assunto e lê o trecho com timestamps → abre o detalhe: player, transcrição clicável, "Baixar" → se falhou, "Reprocessar transcrição".
|
||||
- **Anti-goals de produto:** não é editor de vídeo/transcrição; não expõe o acervo; não publica nada; não cria cadastro de pessoa; não é CMS de vídeo genérico.
|
||||
|
||||
### Esboço de fluxo (C)
|
||||
|
||||
```text
|
||||
[acervo /campanha/comunicacao/acervo]
|
||||
→ alternador de fonte: "Falas da Câmara" | "Gravações enviadas"
|
||||
→ "Enviar gravação" → arquivo local + título/data → estado visível na lista
|
||||
→ (Enviando → Processando → Pronto | Falhou)
|
||||
→ Pronto: busca textual alcança a fala de qualquer pessoa → abre o detalhe
|
||||
→ player + transcrição clicável (clique posiciona o player) + "Baixar"
|
||||
→ Falhou: "Reprocessar transcrição"
|
||||
[outcome: gravação própria pesquisável no acervo, sem depender de fora]
|
||||
```
|
||||
|
||||
### Design UI (C)
|
||||
|
||||
- Design UI (gate): `docs/plans/acervo-gravacoes-enviadas-ui-design.html` — alternador de fonte, envio com estados, lista e detalhe (player + transcrição + download), na linguagem visual do `/campanha`. Por que C: fluxo novo (upload + nova fonte + detalhe), mesmo sem rota de topo nova.
|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- `communicator` — e `coordinator`/`candidate` — envia um arquivo de vídeo local e acompanha o processamento até "Pronto"; `advisor`/`leader` não têm acesso (fail-closed).
|
||||
- A gravação vira uma fonte do acervo em `/campanha/comunicacao/acervo`, com alternador visível "Falas da Câmara" × "Gravações enviadas"; a fonte nova tem busca própria, sem as facetas da Câmara.
|
||||
- A busca textual encontra a fala de qualquer pessoa da gravação, com trecho e timestamps; a unidade do resultado é a gravação.
|
||||
- Detalhe da gravação: player, transcrição clicável por segmento (clique posiciona o player, como no acervo atual), "Baixar" e "Reprocessar transcrição" no estado "Falhou".
|
||||
- Estados de processamento visíveis e honestos; a falha preserva o arquivo e permite reprocessar.
|
||||
- Gravação só é servida para quem tem acesso ao acervo — nunca URL pública — reusando o dono do mecanismo de mídia privada definido no C193.
|
||||
- **Guardrails:** sem diarização/rotulagem de pessoa (C200), sem corte/clipe, sem publicação externa, sem segundo cadastro de pessoa, sem score/vaidade; transcrição somente leitura em v1.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** N/A — resultados de busca não são métrica nem agregado; não desbloqueiam decisão numérica.
|
||||
- **Decisões desbloqueadas:** N/A — a escolha da persona ("qual fala/trecho usar") é qualitativa.
|
||||
- **Forma:** N/A — restrição de produto: nenhum score de relevância/similaridade exposto como número.
|
||||
|
||||
## Dados da decisão (literais)
|
||||
|
||||
- **Papel:** `communicator` = "Assessor de Comunicação"; `coordinator`/`candidate` também. Mesmo gate de leitura do acervo (`speechCatalog`); `advisor`/`leader` fora.
|
||||
- **Fontes (rótulos):** "Falas da Câmara" (acervo atual) e "Gravações enviadas" (novo), no mesmo `/campanha/comunicacao/acervo` via alternador; detalhe da gravação em rota própria sob o acervo, `/campanha/comunicacao/acervo/gravacoes/[id]`.
|
||||
- **Gestos:** "Enviar gravação" (upload) · "Reprocessar transcrição" · "Baixar" (arquivo) · player no detalhe.
|
||||
- **Estados de processamento:** "Enviando" (arquivo subindo; ainda não pesquisável) · "Processando" (transcrevendo; ainda não pesquisável) · "Pronto" (pesquisável: player, transcrição e download) · "Falhou" (transcrição não saiu; arquivo preservado e "Reprocessar transcrição" disponível). A gravação aparece na lista com o estado desde o envio.
|
||||
- **Transcrição:** timestamps por segmento; clique no segmento posiciona o player — mesmo comportamento do acervo atual.
|
||||
- **Entrada:** arquivo de vídeo local (mp4/mov/mkv e afins); erro claro acima do limite; gravações de horas são o caso real — o valor do limite é decisão da implementação.
|
||||
- **Apagar:** com confirmação; remove a gravação (e sua transcrição/mídia) sem tocar nas falas da Câmara.
|
||||
- **Dependência dura:** C193 (mídia privada/servir autenticado); C200 (separação por pessoa) depende deste.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** `src/app/(campaign)/campanha/(app)/comunicacao/acervo/` (página atual + sub-rota nova), `src/components/campaign/speech/` (alternador, lista, filtros, player, detalhe), `src/utilities/speech/` (`speechPageData.ts`, `speechListFilters.ts`, `speechListUrl.ts`, `speechViewModels.ts`), `src/utilities/access/speeches.ts` (leitura), `src/utilities/campaignPageActor.ts` (gate `speechCatalog`), esteira de mídia (`Media`/`mediaStorage.ts` como referência, respeitando o dono do C193).
|
||||
- **Precedente a olhar:** C174 (segunda fonte já entra na mesma busca via OR de ids), `speechCutJob.ts` (status + step + disparo pós-resposta + reaper preguiçoso), `speechPosterJob.ts` (single-flight), import CSV de apoiadores (enviar → processar → confirmar), route handler multipart `src/app/(campaign)/campanha/api/ai-transcribe/route.ts`, CLI da Câmara (`scripts/import-camara-speeches.mjs` + `scripts/lib/camaraFetch.mjs`) para o padrão de ASR com timestamps.
|
||||
- **Risco de acoplamento:** (1) mídia privada é do C193 — não criar mecanismo gêmeo; `Media` continua leitura pública (contrato pinado em teste); (2) upload de vídeo de horas não cabe no default de server action (1 MB) — decisão do plano de implementação; (3) regra de chrome/regex do acervo pode capturar `gravacoes` como id (mesma armadilha anotada no C194); (4) nav pinada `[Acervo, Cortes]` e C184/#1106 mexem nos mesmos arquivos — nada de item de nav novo; (5) contrato de URL da lista atual (`q|year|topic|scope|phase|municipality|duration|page`) não pode quebrar deep-links.
|
||||
|
||||
## Dependências
|
||||
|
||||
- **C193 (dura):** mecanismo de mídia privada e servir autenticado. O C199 pode possuir o upload de arquivo grande; o _servir privado_ é do C193 — sem ele fechado, a fatia privada não entra.
|
||||
- **C153/C154 (entregues):** base do acervo (dados, busca, rota, gate).
|
||||
- **C184/#1106 (soft):** conflito de nav/sub-itens — serializar.
|
||||
- **C200 (futuro):** depende deste; não bloqueia a fatia.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Diarização/rotulagem por pessoa (C200).
|
||||
- Link/YouTube/download automático por URL.
|
||||
- Cortes/clipes (C167/C174 são da Câmara).
|
||||
- Busca semântica (C192).
|
||||
- Edição de transcrição.
|
||||
- Publicação externa/espelhamento em rede social.
|
||||
- Dossiês/Sollinha.
|
||||
- Comissões/outras fontes e classificação por tema/alcance.
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **Virar editor.** Se alguém "só completar": cortar, corrigir, legendar, remixar. **Corte neste item:** transcrição somente leitura; a única "edição" é reprocessar a transcrição inteira.
|
||||
- **Diarização no v1.** Se alguém "só completar": rotular quem falou em horas de áudio. **Corte neste item:** C200; a busca textual geral já resolve o job.
|
||||
- **Espelhar/publicar.** Se alguém "só completar": derivar para redes ou galeria. **Corte neste item:** arquivo interno; nada sai do acervo.
|
||||
- **Segundo cadastro de pessoa.** Se alguém "só completar": transformar falante em `Contact`. **Corte neste item:** sem rótulo de pessoa em v1.
|
||||
- **Merge unificado das fontes.** Se alguém "só completar": lista única Câmara+gravações e entrada no `home-search`. **Corte neste item:** alternador dentro do acervo; a busca global não entra.
|
||||
- **Fila/infra de processamento.** Se alguém "só completar": pipeline genérico, tempo real, chunking. **Corte neste item:** processamento inteiro com estado visível.
|
||||
|
||||
## Questões em aberto (produto)
|
||||
|
||||
- **Forma da segunda fonte?** **Opções:** A) duas fontes no mesmo acervo, com alternador e busca própria | B) merge das duas numa lista única | C) rota separada fora do acervo. **Recomendação:** A — as facetas da Câmara não se aplicam à gravação e o merge multi-coleção é caro; o alternador mantém "acervo pesquisável" sem fingir homogeneidade. _(assumido — validar no gate)_
|
||||
- **Transcrição editável em v1?** **Opções:** A) somente leitura | B) correção manual inline. **Recomendação:** A — correção vira curadoria; se doer, item próprio. _(assumido — validar no gate)_
|
||||
- **Quem apaga uma gravação?** **Opções:** A) qualquer um dos três papéis com acesso, com confirmação | B) só `coordinator`/`candidate`. **Recomendação:** A — a assessoria é dona do acervo no dia a dia; apagar não toca nas falas da Câmara e é confirmado. _(assumido — validar no gate)_
|
||||
- **Gravação que falhou fica visível?** **Opções:** A) sim, com "Falhou" e "Reprocessar transcrição" | B) só aparece quando "Pronto". **Recomendação:** A — falha silenciosa faz o arquivo sumir sem explicação. _(assumido — validar no gate)_
|
||||
- **O que a assessoria preenche ao enviar?** **Opções:** A) título obrigatório + data da gravação | B) só o arquivo. **Recomendação:** A — sem isso a lista vira pilha de `IMG_…`; uploader e duração são automáticos. _(assumido — validar no gate)_
|
||||
|
||||
## Referências
|
||||
|
||||
- GitHub Issue #1166
|
||||
- Design UI (gate): `docs/plans/acervo-gravacoes-enviadas-ui-design.html` (+ assets em `docs/plans/acervo-gravacoes-enviadas-ui-design-assets/`)
|
||||
- Planos irmãos: `docs/plans/catalogo-falas-solla.md` (C153) · `docs/plans/acervo-videos-comunicacao.md` (C154) · `docs/plans/acervo-busca-semantica.md` (C192) · `docs/plans/reels-reel-privado.md` (C193) · `docs/plans/reels-biblioteca.md` (C194)
|
||||
- Arquivos-pista: `src/app/(campaign)/campanha/(app)/comunicacao/acervo/page.tsx` · `src/components/campaign/speech/` · `src/utilities/speech/speechPageData.ts` · `src/utilities/speech/speechListFilters.ts` · `src/utilities/speech/speechListUrl.ts` · `src/lib/speechSearch.ts` · `src/collections/SpeechSegment.ts` · `src/utilities/speech/speechCutJob.ts` · `scripts/import-camara-speeches.mjs`
|
||||
- `AGENTS.md` — RBAC de `/campanha`, acervo interno e leader lockdown
|
||||
701
docs/plans/acervo-separacao-por-pessoa-ui-design.html
Normal file
701
docs/plans/acervo-separacao-por-pessoa-ui-design.html
Normal file
|
|
@ -0,0 +1,701 @@
|
|||
<!doctype html>
|
||||
<html lang="pt-BR" data-theme="campaign">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<title>Design — Separar falas por pessoa</title>
|
||||
<style type="text/tailwindcss">
|
||||
@theme {
|
||||
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
--color-background: #ffffff;
|
||||
--color-foreground: #1c1917;
|
||||
--color-card: #ffffff;
|
||||
--color-primary: #c51414;
|
||||
--color-primary-foreground: #ffffff;
|
||||
--color-secondary: #f5f5f4;
|
||||
--color-secondary-foreground: #292524;
|
||||
--color-muted: #f4f4f5;
|
||||
--color-muted-foreground: #6b7280;
|
||||
--color-border: rgb(28 25 23 / 12%);
|
||||
--color-input: #d6d3d1;
|
||||
--color-destructive: #b42318;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.icon-sm {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
min-height: 2.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #c51414;
|
||||
color: white;
|
||||
border: 1px solid #c51414;
|
||||
}
|
||||
.btn-outline {
|
||||
background: white;
|
||||
color: #292524;
|
||||
border: 1px solid #d6d3d1;
|
||||
}
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: #292524;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.field {
|
||||
min-height: 2.75rem;
|
||||
width: 100%;
|
||||
border: 1px solid #d6d3d1;
|
||||
border-radius: 0.5rem;
|
||||
background: white;
|
||||
padding: 0.625rem 0.75rem;
|
||||
outline: none;
|
||||
}
|
||||
.field:focus {
|
||||
border-color: #c51414;
|
||||
box-shadow: 0 0 0 3px rgb(197 20 20 / 18%);
|
||||
}
|
||||
.scene-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #57534e;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.scene-label::after {
|
||||
content: '';
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
background: #d6d3d1;
|
||||
}
|
||||
.desktop-frame {
|
||||
width: 1280px;
|
||||
min-height: 720px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d6d3d1;
|
||||
border-radius: 14px;
|
||||
background: white;
|
||||
box-shadow: 0 18px 45px rgb(28 25 23 / 10%);
|
||||
}
|
||||
.mobile-frame {
|
||||
width: 390px;
|
||||
min-height: 790px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d6d3d1;
|
||||
border-radius: 28px;
|
||||
background: white;
|
||||
box-shadow: 0 18px 45px rgb(28 25 23 / 12%);
|
||||
}
|
||||
.speaker-block {
|
||||
border: 1px solid rgb(28 25 23 / 12%);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
.speaker-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
border-bottom: 1px solid rgb(28 25 23 / 12%);
|
||||
background: #fafaf9;
|
||||
padding: 0.625rem 0.75rem;
|
||||
}
|
||||
.seg {
|
||||
display: grid;
|
||||
grid-template-columns: 3.25rem 1fr;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.55rem 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
.seg:hover,
|
||||
.seg-active {
|
||||
background: #f4f4f5;
|
||||
}
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
min-height: 1.75rem;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
border-radius: 0.375rem;
|
||||
background: #f4f4f5;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-stone-100 text-foreground antialiased">
|
||||
<svg aria-hidden="true" class="absolute size-0 overflow-hidden">
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="m6 4 14 8-14 8z" />
|
||||
</symbol>
|
||||
<symbol id="i-edit" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z" />
|
||||
</symbol>
|
||||
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 21a8 8 0 0 1 16 0" />
|
||||
</symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M12 11v5M12 8h.01" />
|
||||
</symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="m18 6-12 12M6 6l12 12" />
|
||||
</symbol>
|
||||
<symbol id="i-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</symbol>
|
||||
<symbol id="i-back" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 3v12m0 0 4-4m-4 4-4-4" />
|
||||
<path d="M5 21h14" />
|
||||
</symbol>
|
||||
<symbol id="i-filter" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M4 6h16M7 12h10M10 18h4" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<main class="mx-auto w-max max-w-none px-10 py-10">
|
||||
<header class="mb-8 max-w-3xl">
|
||||
<p class="text-sm font-semibold text-primary">Teqo · acervo de comunicação</p>
|
||||
<h1 class="mt-1 text-2xl font-semibold tracking-tight">Separar falas por pessoa</h1>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
Referência hi-fi para agrupamentos acústicos e identificação humana. O produto nunca
|
||||
sugere nem infere quem falou.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- CENA 1 -->
|
||||
<section class="mb-12" aria-labelledby="scene-1">
|
||||
<h2 id="scene-1" class="scene-label">
|
||||
Cena 1 · Desktop — detalhe com transcrição separada por falante
|
||||
</h2>
|
||||
<div class="desktop-frame">
|
||||
<header class="flex min-h-11 items-center gap-3 border-b border-border px-4">
|
||||
<span class="text-muted-foreground">☰</span>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Gravação enviada</p>
|
||||
<p class="text-xs text-muted-foreground">Acervo</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-6">
|
||||
<button class="btn btn-ghost -ml-2">
|
||||
<svg class="icon"><use href="#i-back" /></svg>Voltar ao acervo
|
||||
</button>
|
||||
<div class="mt-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Título da gravação</h3>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
</div>
|
||||
<span class="rounded-full bg-green-100 px-2 py-1 text-xs font-semibold text-green-700"
|
||||
>Pronto</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-5 grid grid-cols-[minmax(0,1.05fr)_minmax(430px,.95fr)] gap-6">
|
||||
<div>
|
||||
<div class="relative grid aspect-video place-items-center rounded-lg bg-stone-900">
|
||||
<button
|
||||
class="grid size-16 place-items-center rounded-full bg-white text-stone-950 shadow-lg"
|
||||
>
|
||||
<svg class="h-7 w-7 translate-x-0.5"><use href="#i-play" /></svg>
|
||||
</button>
|
||||
<div
|
||||
class="absolute inset-x-4 bottom-3 flex items-center gap-3 text-xs text-white"
|
||||
>
|
||||
<span>00:00</span>
|
||||
<div class="h-1 flex-1 rounded-full bg-white/30">
|
||||
<div class="h-full w-[22%] rounded-full bg-white"></div>
|
||||
</div>
|
||||
<span>—:—:—</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary mt-3">
|
||||
<svg class="icon"><use href="#i-download" /></svg>Baixar
|
||||
</button>
|
||||
<div class="mt-4 flex gap-2 rounded-lg border border-border bg-muted/50 p-3">
|
||||
<svg class="icon mt-0.5 shrink-0 text-muted-foreground">
|
||||
<use href="#i-info" />
|
||||
</svg>
|
||||
<p class="text-xs leading-5 text-muted-foreground">
|
||||
Os falantes são agrupados automaticamente pelo áudio. A identificação de cada
|
||||
pessoa é feita pela equipe.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<section aria-label="Transcrição por falante">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-xs font-medium text-muted-foreground">TRANSCRIÇÃO POR FALANTE</h4>
|
||||
<span class="text-xs text-muted-foreground">Clique para posicionar</span>
|
||||
</div>
|
||||
<div class="mt-2 space-y-3">
|
||||
<article class="speaker-block">
|
||||
<header class="speaker-head">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="grid size-7 place-items-center rounded-full bg-muted"
|
||||
><svg class="icon-sm"><use href="#i-user" /></svg
|
||||
></span>
|
||||
<div>
|
||||
<h5 class="text-sm font-semibold">Falante 1</h5>
|
||||
<p class="text-[11px] text-muted-foreground">
|
||||
Agrupamento sem identificação
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline">
|
||||
<svg class="icon-sm"><use href="#i-edit" /></svg>Identificar falante
|
||||
</button>
|
||||
</header>
|
||||
<button class="seg seg-active">
|
||||
<span class="text-xs tabular-nums text-primary">00:00</span
|
||||
><span class="text-sm leading-5"
|
||||
>Texto transcrito deste agrupamento aparece neste trecho.</span
|
||||
></button
|
||||
><button class="seg">
|
||||
<span class="text-xs tabular-nums text-muted-foreground">00:38</span
|
||||
><span class="text-sm leading-5"
|
||||
>Outra fala da mesma voz permanece no mesmo agrupamento.</span
|
||||
>
|
||||
</button>
|
||||
</article>
|
||||
<article class="speaker-block">
|
||||
<header class="speaker-head">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="grid size-7 place-items-center rounded-full bg-primary/10 text-primary"
|
||||
><svg class="icon-sm"><use href="#i-user" /></svg
|
||||
></span>
|
||||
<div>
|
||||
<h5 class="text-sm font-semibold">Nome do falante</h5>
|
||||
<p class="text-[11px] text-muted-foreground">
|
||||
Identificação feita pela equipe
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline">
|
||||
<svg class="icon-sm"><use href="#i-edit" /></svg>Editar identificação
|
||||
</button>
|
||||
</header>
|
||||
<button class="seg">
|
||||
<span class="text-xs tabular-nums text-muted-foreground">00:18</span
|
||||
><span class="text-sm leading-5"
|
||||
>O nome aplicado identifica todas as falas deste agrupamento.</span
|
||||
>
|
||||
</button>
|
||||
</article>
|
||||
<article class="speaker-block">
|
||||
<header class="speaker-head">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="grid size-7 place-items-center rounded-full bg-muted"
|
||||
><svg class="icon-sm"><use href="#i-user" /></svg
|
||||
></span>
|
||||
<div>
|
||||
<h5 class="text-sm font-semibold">Falante 3</h5>
|
||||
<p class="text-[11px] text-muted-foreground">
|
||||
Agrupamento sem identificação
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline">
|
||||
<svg class="icon-sm"><use href="#i-edit" /></svg>Identificar falante
|
||||
</button>
|
||||
</header>
|
||||
<button class="seg">
|
||||
<span class="text-xs tabular-nums text-muted-foreground">01:04</span
|
||||
><span class="text-sm leading-5"
|
||||
>Texto transcrito do terceiro agrupamento.</span
|
||||
>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 2 -->
|
||||
<section class="mb-12" aria-labelledby="scene-2">
|
||||
<h2 id="scene-2" class="scene-label">Cena 2 · Desktop — diálogo para rotular o falante</h2>
|
||||
<div class="desktop-frame relative grid place-items-center bg-stone-200/70 p-8">
|
||||
<div class="absolute inset-0 bg-stone-950/25"></div>
|
||||
<section
|
||||
class="relative w-full max-w-md rounded-xl border border-border bg-white p-6 shadow-2xl"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="identify-title"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 id="identify-title" class="text-base font-semibold">Identificar falante</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Dê um nome ao agrupamento “Falante 1”.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="grid size-8 place-items-center rounded-md hover:bg-muted"
|
||||
aria-label="Fechar"
|
||||
>
|
||||
<svg class="icon"><use href="#i-x" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-5 rounded-lg border border-border bg-muted/50 p-3">
|
||||
<div class="flex gap-2">
|
||||
<svg class="icon mt-0.5 shrink-0 text-muted-foreground"><use href="#i-info" /></svg>
|
||||
<p class="text-xs leading-5 text-muted-foreground">
|
||||
Ouça um trecho e informe quem é a pessoa. O acervo não sugere nem identifica
|
||||
pessoas automaticamente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<label for="speaker-name" class="text-sm font-medium">Nome do falante</label
|
||||
><input
|
||||
id="speaker-name"
|
||||
class="field mt-1.5 text-sm"
|
||||
placeholder="Digite o nome do falante"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-muted-foreground">
|
||||
O nome será aplicado a todas as falas deste agrupamento nesta gravação.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<button class="btn btn-outline">Cancelar</button
|
||||
><button class="btn btn-primary">Salvar identificação</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 3 -->
|
||||
<section class="mb-12" aria-labelledby="scene-3">
|
||||
<h2 id="scene-3" class="scene-label">
|
||||
Cena 3 · Desktop — lista com faceta “Pessoa” aplicada
|
||||
</h2>
|
||||
<div class="desktop-frame flex">
|
||||
<aside class="flex w-[220px] shrink-0 flex-col border-r border-border bg-stone-50 p-3">
|
||||
<div class="flex h-12 items-center gap-2 px-2 font-semibold">
|
||||
<span class="grid size-8 place-items-center rounded-lg bg-primary text-sm text-white"
|
||||
>T</span
|
||||
>
|
||||
Campanha
|
||||
</div>
|
||||
<nav class="mt-3 space-y-1 text-sm">
|
||||
<div class="rounded-md px-3 py-2 text-muted-foreground">Início</div>
|
||||
<div class="rounded-md px-3 py-2 text-muted-foreground">Municípios</div>
|
||||
<div class="rounded-md px-3 py-2 text-muted-foreground">Pessoas</div>
|
||||
<div
|
||||
class="rounded-md bg-white px-3 py-2 font-medium text-primary ring-1 ring-border"
|
||||
>
|
||||
Comunicação
|
||||
</div>
|
||||
<div class="ml-5 border-l border-border py-1 pl-3">
|
||||
<div
|
||||
class="rounded-md bg-white px-3 py-2 font-medium text-primary ring-1 ring-border"
|
||||
>
|
||||
Acervo
|
||||
</div>
|
||||
<div class="px-3 py-2 text-muted-foreground">Cortes</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<div class="min-w-0 flex-1">
|
||||
<header class="flex min-h-11 items-center gap-3 border-b border-border px-4">
|
||||
<span class="text-muted-foreground">☰</span>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Acervo</p>
|
||||
<p class="text-xs text-muted-foreground">Comunicação</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">Acervo de falas</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Gravações enviadas</p>
|
||||
</div>
|
||||
<div class="mt-5 inline-flex rounded-lg bg-secondary p-1 text-sm">
|
||||
<span class="rounded-md px-3 py-2 text-muted-foreground">Falas da Câmara</span
|
||||
><span
|
||||
class="rounded-md bg-white px-3 py-2 font-medium shadow-sm ring-1 ring-border"
|
||||
>Gravações enviadas</span
|
||||
>
|
||||
</div>
|
||||
<label class="mt-5 block text-sm font-medium">Buscar nas gravações enviadas</label>
|
||||
<div
|
||||
class="mt-1.5 flex min-h-11 items-center gap-2 rounded-lg border border-input px-3"
|
||||
>
|
||||
<svg class="icon text-muted-foreground"><use href="#i-search" /></svg
|
||||
><span class="text-sm text-muted-foreground"
|
||||
>Busque por palavra ou trecho da transcrição…</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<button class="btn btn-outline h-8 min-h-8 px-3">
|
||||
<svg class="icon-sm"><use href="#i-filter" /></svg>Pessoa: Nome do falante
|
||||
<svg class="icon-sm"><use href="#i-x" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-5 space-y-3">
|
||||
<article class="rounded-xl border border-border p-4">
|
||||
<div class="grid grid-cols-[112px_minmax(0,1fr)_auto] gap-4">
|
||||
<div class="aspect-video rounded-lg bg-stone-200"></div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h4 class="text-sm font-semibold">Título da gravação</h4>
|
||||
<span
|
||||
class="rounded-full bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-700"
|
||||
>Pronto</span
|
||||
>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
<p class="mt-2 text-sm leading-5">
|
||||
Trecho da transcrição encontrado pela busca geral.
|
||||
</p>
|
||||
<div class="mt-2 flex items-center gap-2 text-xs">
|
||||
<span class="chip"
|
||||
><svg class="icon-sm"><use href="#i-user" /></svg>Nome do falante</span
|
||||
><span class="text-muted-foreground">aparece nesta gravação</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline self-center">
|
||||
Abrir<svg class="icon-sm"><use href="#i-chevron" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<article class="rounded-xl border border-border p-4">
|
||||
<div class="grid grid-cols-[112px_minmax(0,1fr)_auto] gap-4">
|
||||
<div class="aspect-video rounded-lg bg-stone-200"></div>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold">Outra gravação enviada</h4>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
<p class="mt-2 text-sm leading-5">
|
||||
Outro trecho continua alcançável pela busca de texto.
|
||||
</p>
|
||||
<div class="mt-2 flex items-center gap-2 text-xs">
|
||||
<span class="chip"
|
||||
><svg class="icon-sm"><use href="#i-user" /></svg>Nome do falante</span
|
||||
><span class="text-muted-foreground">aparece nesta gravação</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline self-center">
|
||||
Abrir<svg class="icon-sm"><use href="#i-chevron" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 4 -->
|
||||
<section class="mb-12" aria-labelledby="scene-4">
|
||||
<h2 id="scene-4" class="scene-label">
|
||||
Cena 4 · Desktop — agrupamento aproximado / qualidade ruim
|
||||
</h2>
|
||||
<div class="desktop-frame p-8">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Título da gravação</h3>
|
||||
<p class="mt-1 text-xs text-muted-foreground">Transcrição por falante</p>
|
||||
</div>
|
||||
<div class="mt-5 flex gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<svg class="icon mt-0.5 shrink-0 text-amber-800"><use href="#i-info" /></svg>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-amber-950">
|
||||
A separação por falante pode estar imprecisa
|
||||
</h4>
|
||||
<p class="mt-1 text-sm leading-5 text-amber-900">
|
||||
O agrupamento é automático e aproximado. Ruído e falas sobrepostas podem colocar
|
||||
trechos no agrupamento errado. A equipe continua responsável por identificar cada
|
||||
falante.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-2 gap-4">
|
||||
<article class="speaker-block">
|
||||
<header class="speaker-head">
|
||||
<div>
|
||||
<h5 class="text-sm font-semibold">Falante 1</h5>
|
||||
<p class="text-[11px] text-muted-foreground">Agrupamento sem identificação</p>
|
||||
</div>
|
||||
<button class="btn btn-outline">
|
||||
<svg class="icon-sm"><use href="#i-edit" /></svg>Identificar falante
|
||||
</button>
|
||||
</header>
|
||||
<button class="seg">
|
||||
<span class="text-xs text-muted-foreground">00:00</span
|
||||
><span class="text-sm leading-5"
|
||||
>Trecho agrupado automaticamente pelo áudio.</span
|
||||
>
|
||||
</button>
|
||||
</article>
|
||||
<article class="speaker-block">
|
||||
<header class="speaker-head">
|
||||
<div>
|
||||
<h5 class="text-sm font-semibold">Falante 2</h5>
|
||||
<p class="text-[11px] text-muted-foreground">Agrupamento sem identificação</p>
|
||||
</div>
|
||||
<button class="btn btn-outline">
|
||||
<svg class="icon-sm"><use href="#i-edit" /></svg>Identificar falante
|
||||
</button>
|
||||
</header>
|
||||
<button class="seg">
|
||||
<span class="text-xs text-muted-foreground">00:19</span
|
||||
><span class="text-sm leading-5"
|
||||
>Outro trecho pode conter sobreposição de vozes.</span
|
||||
>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 5 -->
|
||||
<section aria-labelledby="scene-5">
|
||||
<h2 id="scene-5" class="scene-label">
|
||||
Cena 5 · Mobile (390 px) — detalhe com agrupamentos e identificação
|
||||
</h2>
|
||||
<div class="mobile-frame relative pb-20">
|
||||
<header class="flex min-h-14 items-center gap-3 bg-primary px-4 text-white">
|
||||
<svg class="icon"><use href="#i-back" /></svg>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold">Gravação enviada</h3>
|
||||
<p class="text-xs text-white/75">Acervo</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<h4 class="text-base font-semibold">Título da gravação</h4>
|
||||
<p class="mt-1 text-xs text-muted-foreground">dd/mm/aaaa · —:—:—</p>
|
||||
</div>
|
||||
<div class="relative mt-4 grid aspect-video place-items-center rounded-lg bg-stone-900">
|
||||
<button class="grid size-12 place-items-center rounded-full bg-white">
|
||||
<svg class="h-5 w-5 translate-x-0.5"><use href="#i-play" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3 flex gap-2 rounded-lg border border-border bg-muted/50 p-3">
|
||||
<svg class="icon mt-0.5 shrink-0 text-muted-foreground"><use href="#i-info" /></svg>
|
||||
<p class="text-xs leading-5 text-muted-foreground">
|
||||
Agrupamento automático e aproximado. A identificação é feita pela equipe.
|
||||
</p>
|
||||
</div>
|
||||
<section class="mt-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h5 class="text-xs font-medium text-muted-foreground">TRANSCRIÇÃO POR FALANTE</h5>
|
||||
<span class="text-[11px] text-muted-foreground">Toque para posicionar</span>
|
||||
</div>
|
||||
<div class="mt-2 space-y-3">
|
||||
<article class="speaker-block">
|
||||
<header class="border-b border-border bg-stone-50 p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="grid size-7 place-items-center rounded-full bg-muted"
|
||||
><svg class="icon-sm"><use href="#i-user" /></svg
|
||||
></span>
|
||||
<h6 class="text-sm font-semibold">Falante 1</h6>
|
||||
</div>
|
||||
<button class="btn btn-outline h-9 min-h-9 px-2">
|
||||
<svg class="icon-sm"><use href="#i-edit" /></svg>Identificar
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<button class="seg seg-active">
|
||||
<span class="text-xs text-primary">00:00</span
|
||||
><span class="text-sm leading-5"
|
||||
>Texto transcrito deste agrupamento.</span
|
||||
></button
|
||||
><button class="seg">
|
||||
<span class="text-xs text-muted-foreground">00:38</span
|
||||
><span class="text-sm leading-5">Outra fala da mesma voz.</span>
|
||||
</button>
|
||||
</article>
|
||||
<article class="speaker-block">
|
||||
<header class="border-b border-border bg-stone-50 p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="grid size-7 place-items-center rounded-full bg-primary/10 text-primary"
|
||||
><svg class="icon-sm"><use href="#i-user" /></svg
|
||||
></span>
|
||||
<div>
|
||||
<h6 class="text-sm font-semibold">Nome do falante</h6>
|
||||
<p class="text-[10px] text-muted-foreground">Identificado pela equipe</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="grid size-9 place-items-center rounded-lg border border-input bg-white"
|
||||
aria-label="Editar identificação"
|
||||
>
|
||||
<svg class="icon-sm"><use href="#i-edit" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<button class="seg">
|
||||
<span class="text-xs text-muted-foreground">00:18</span
|
||||
><span class="text-sm leading-5"
|
||||
>Todas as falas deste agrupamento recebem o mesmo nome.</span
|
||||
>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<nav
|
||||
class="absolute inset-x-0 bottom-0 grid h-16 grid-cols-5 border-t border-border bg-white pt-2 text-[10px] text-muted-foreground"
|
||||
>
|
||||
<span class="text-center">Início</span><span class="text-center">Pessoas</span
|
||||
><span class="text-center text-primary">Comunicação</span
|
||||
><span class="text-center">Agenda</span><span class="text-center">Mais</span>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
115
docs/plans/acervo-separacao-por-pessoa.md
Normal file
115
docs/plans/acervo-separacao-por-pessoa.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# C200 — Separar falas por pessoa nas gravações enviadas
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-09-18
|
||||
Issue: #1167
|
||||
Priority: P3
|
||||
Impeccable: C — fluxo novo no acervo (rotulagem de falantes no detalhe + faceta "Pessoa" na fonte de gravações enviadas)
|
||||
Design UI: docs/plans/acervo-separacao-por-pessoa-ui-design.html
|
||||
Appetite: ~2–3 dias eng; um outcome verificável — a assessoria identifica quem fala numa gravação enviada e filtra o acervo pelas falas de uma pessoa
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
As gravações enviadas ao acervo (C199) não têm só fala do Solla: plenárias e debates têm várias vozes, e a transcrição sai como um bloco único. A busca geral já cobre esse texto inteiro, mas não responde à pergunta que a assessoria faz o tempo todo: "o que **fulano** disse nesta gravação?". Este item é a camada de quem-falou-o-quê: a transcrição passa a vir dividida por agrupamento de falante, a assessoria identifica quem é cada voz e o acervo ganha um recorte por pessoa.
|
||||
|
||||
O guardrail é duro e vem do próprio pedido: separar por áudio é **agrupamento acústico** ("Falante 1", "Falante 2", …), e a identificação é **humana** — a assessoria ouve e rotula. Nada de voiceprint, nada de casar voz com pessoa conhecida, nada de identificar terceiros por biometria. Rótulo não preenchido continua "Falante N"; nunca se inventa identidade.
|
||||
|
||||
P3: melhora real o garimpo, mas depende do C199 e não bloqueia operação — o acervo segue útil com a busca geral enquanto isso.
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** assessoria de comunicação (`communicator`) — e também `coordinator`/`candidate` — na mesa, montando peça com prazo curto, garimpando em plenárias/debates enviados pela equipe; não é editor de transcrição nem curador de cadastro.
|
||||
- **Job principal:** achar as falas de uma pessoa específica dentro de uma gravação e conseguir recortar as gravações em que ela fala.
|
||||
- **Fluxo desejado:**
|
||||
1. Abre o acervo na fonte "Gravações enviadas" e abre uma gravação.
|
||||
2. A transcrição aparece dividida em "Falante 1", "Falante 2", … — sem nome inventado, sem sugestão automática de identidade.
|
||||
3. Ouve o trecho e usa "Identificar falante" para rotular o agrupamento (ex.: "Falante 2" → "Dep. Jorge Solla"); o rótulo vale para todas as falas daquele agrupamento naquela gravação.
|
||||
4. Volta à lista e usa a faceta "Pessoa" para ver só as gravações com falas daquela pessoa; a busca geral continua cobrindo a transcrição inteira.
|
||||
5. Abre o trecho e assiste/baixa como já faz hoje.
|
||||
- **Anti-goals de produto:** não vira biometria/identificação automática de voz; não cria cadastro de pessoas paralelo ao `Contact`; não troca nem esvazia a busca geral; não edita transcrição; não reprocessa o acervo antigo sozinho.
|
||||
|
||||
### Esboço de fluxo (C)
|
||||
|
||||
```text
|
||||
[acervo: fonte "Gravações enviadas"] → abre a gravação
|
||||
→ transcrição dividida em "Falante 1", "Falante 2", … (sem nome inventado)
|
||||
→ ouve + "Identificar falante" → rótulo vale para todas as falas daquele agrupamento
|
||||
→ volta à lista → faceta "Pessoa" recorta as gravações com falas daquela pessoa
|
||||
→ abre o trecho, assiste/baixa → [outcome: a fala da pessoa certa na mão]
|
||||
```
|
||||
|
||||
### Design UI (C)
|
||||
|
||||
- Design UI (gate): `docs/plans/acervo-separacao-por-pessoa-ui-design.html` (+ assets em `docs/plans/acervo-separacao-por-pessoa-ui-design-assets/`) — a produzir pelo designer.
|
||||
- Por que C: há affordance nova no detalhe (rotular agrupamento) e estado novo na lista (faceta "Pessoa" só na fonte de gravações enviadas, com rótulos por gravação), dentro da tela existente. Design hi-fi obrigatório no gate e fonte de verdade do port.
|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- Gravações enviadas (C199) têm a transcrição dividida por agrupamento de falante; o rótulo default é "Falante 1", "Falante 2", … (sempre com número).
|
||||
- A assessoria rotula um agrupamento com "Identificar falante" no detalhe da gravação; o rótulo passa a valer para todas as falas daquele agrupamento naquela gravação.
|
||||
- A faceta "Pessoa" existe na fonte "Gravações enviadas" e recorta as gravações em que aquele rótulo aparece; a busca geral continua alcançando a transcrição inteira, como hoje.
|
||||
- Rótulo não preenchido permanece "Falante N"; o produto nunca sugere, infere ou completa identidade.
|
||||
- Reprocessar a transcrição de uma gravação não perde os rótulos já feitos (ou exige re-vinculação clara e avisada) — requisito de produto, não detalhe técnico.
|
||||
- Escopo de aplicação: gravações novas a partir deste item; reprocessar/diarizar as antigas é ação explícita, nunca automática.
|
||||
- **Guardrails:** sem biometria de voz; identificação é humana; sem segundo cadastro de pessoa (rótulo é texto, não `Contact`); gate fail-closed do acervo (`communicator` + `coordinator`/`candidate`; demais negados); acervo segue interno; sem score de confiança de "quem falou" na UI.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** N/A — o filtro por pessoa é recorte de conteúdo/conteúdo editorial, não métrica nem agregado.
|
||||
- **Decisões desbloqueadas:** N/A — a escolha da persona ("qual fala usar") é qualitativa.
|
||||
- **Forma:** N/A — sem superfície de dados; restrição de produto: agrupamento é acústico e aproximado, então nada de confiança numérica por falante (número cru engana e sugere precisão que não existe).
|
||||
|
||||
## Dados da decisão (literais)
|
||||
|
||||
- **Rótulo default dos agrupamentos:** "Falante 1", "Falante 2", … (sempre com número; nunca nome inventado).
|
||||
- **Ação de curadoria:** "Identificar falante" (rotular), no detalhe da gravação.
|
||||
- **Faceta:** "Pessoa", na fonte "Gravações enviadas" do acervo.
|
||||
- **Fonte:** "Gravações enviadas" (a fonte que o C199 popula).
|
||||
- **Papéis:** `communicator` ("Assessor de Comunicação") + `coordinator`/`candidate`; demais negados (fail-closed) — mesmo gate do acervo.
|
||||
- **Guardrail literal:** sem biometria de voz; identificação humana; sem segundo cadastro de pessoa.
|
||||
- **Dependência dura:** C199.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** `src/app/(campaign)/campanha/(app)/comunicacao/acervo/…` (lista + detalhe); `src/components/campaign/speech/SpeechAcervoFilters.tsx` e `src/components/campaign/shared/CampaignHeaderFilterPopover.tsx` (faceta multi-seleção reutilizável); `src/utilities/speech/speechListUrl.ts` (`SpeechFilterOptions` e params/URLs da lista), `speechListFilters.ts`, `speechOmnibox.ts`, `speechPageData.ts`; `src/utilities/ai/deepInfraTranscribe.ts` e o import (`src/utilities/speech/speechImport.ts`); `src/utilities/access/speeches.ts` (gate).
|
||||
- **Precedente a olhar:** C154 (filtros/facetas do acervo), C153 (menções como texto; anti-goal de normalizar pessoa), C192 (recorte adicional na mesma tela), `src/collections/SpeechSegment.ts` (segmento hoje sem locutor, admin-hidden, substituído em bloco no import).
|
||||
- **Risco de acoplamento:** o reprocessamento do import substitui os segmentos em bloco — um rótulo por trecho só sobrevive se o reprocessamento carregar/re-aplicar; o contrato de URL da lista sustenta deep-links e a faceta nova não pode quebrar as existentes; busca geral, cortes e gate de leitura intocados.
|
||||
|
||||
## Dependências
|
||||
|
||||
- **C199** (dura, bloqueante) — sem as gravações transcritas não há o que separar.
|
||||
- Soft: C192 (busca semântica) compartilha a tela e a organização de filtros; não bloqueia nem é pré-requisito.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Biometria/voiceprint e qualquer identificação automática de pessoa.
|
||||
- `Contact`/segundo cadastro de pessoa.
|
||||
- Edição/correção de transcrição e correção de erros de ASR.
|
||||
- Cortes/clipes e biblioteca de cortes.
|
||||
- Busca semântica (C192) — recorte ortogonal sobre a mesma tela.
|
||||
- Publicação externa, compartilhamento e Sollinha/dossiês.
|
||||
- Backfill automático de diarização nas gravações antigas.
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **Virar reconhecimento biométrico.** Se alguém "só completar": voiceprint, match automático voz↔pessoa, identificação de terceiros. **Corte neste item:** agrupamento acústico + rótulo humano; nada automático.
|
||||
- **Cadastro de pessoas/vozes.** Se alguém "só completar": diretório de falantes, entidade nova, merge entre gravações. **Corte:** rótulo é texto por gravação; sem unificação automática.
|
||||
- **Transcrição perfeita por falante.** Se alguém "só completar": correção de sobreposição, refino de palavras, edição de texto. **Corte:** agrupamento é aproximado; fala sobreposta pode cair no agrupamento errado e a assessoria re-rotula.
|
||||
- **Identificar automaticamente autoridades.** Se alguém "só completar": inferir "quem presidia" ou "o deputado" por heurística. **Corte:** se quiserem marcar presidência, é rótulo humano como qualquer outro.
|
||||
|
||||
## Questões em aberto (produto)
|
||||
|
||||
- **Qual provedor/etapa faz a diarização?** **Opções:** A) provedor com diarização nativa no pipeline de transcrição | B) etapa de diarização sobre o áudio já transcrito | C) diarização local no homeserver. **Recomendação:** decisão do plano de implementação, com custo por minuto medido e fallback honesto — o ASR atual (Whisper large-v3) **não** entrega diarização; sem a etapa, a transcrição volta a bloco único (nunca fingir separação). _(assumido — validar no gate)_
|
||||
- **Reprocessar as gravações antigas?** **Opções:** A) backfill automático de todo o acervo | B) ação explícita por gravação | C) não oferecer. **Recomendação:** B — o custo por minuto não justifica varrer tudo agora, e reprocessar pode mexer em rótulos já feitos; quando rodar, é avisado e não perde rótulo. _(assumido — validar no gate)_
|
||||
- **Mesma pessoa em gravações diferentes?** **Opções:** A) unificação automática por voz (biometria — proibida) | B) texto livre por gravação, sem unificação; o filtro casa pelo texto do rótulo | C) diretório sugerido + merge manual (item futuro). **Recomendação:** B — sem segunda fonte de identidade nem matching biométrico; se as variações de nome ("Solla" vs "Dep. Jorge Solla") fragmentarem o filtro, é curadoria, outro item se doer. _(assumido — validar no gate)_
|
||||
- **O que mostrar quando o agrupamento é ruim (sobreposição/ruído)?** **Opções:** A) esconder a separação quando a qualidade for baixa | B) mostrar mesmo aproximada, com aviso discreto de que o agrupamento é automático | C) permitir marcar "não sei quem falou". **Recomendação:** B (+ C se couber no design) — transparência sobre o limite; esconder quebra a confiança no que apareceu. _(assumido — validar no gate)_
|
||||
|
||||
## Referências
|
||||
|
||||
- GitHub Issue #1167
|
||||
- Design UI (gate): `docs/plans/acervo-separacao-por-pessoa-ui-design.html` (+ assets em `docs/plans/acervo-separacao-por-pessoa-ui-design-assets/`)
|
||||
- Dependência: C199 (upload de gravações no acervo — a definir planos irmãos)
|
||||
- Planos irmãos: `docs/plans/acervo-videos-comunicacao.md` (C154), `docs/plans/catalogo-falas-solla.md` (C153), `docs/plans/acervo-busca-semantica.md` (C192)
|
||||
- Arquivos-pista: `src/components/campaign/speech/SpeechAcervoFilters.tsx`, `src/components/campaign/shared/CampaignHeaderFilterPopover.tsx`, `src/utilities/speech/speechListUrl.ts`, `src/utilities/speech/speechImport.ts`, `src/collections/SpeechSegment.ts`, `src/utilities/ai/deepInfraTranscribe.ts`
|
||||
- `AGENTS.md` — RBAC de `/campanha`, acervo interno, LGPD/consentimento fail-closed
|
||||
Loading…
Reference in a new issue