@fayz-ai/plugin-scribe 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/components/DraftPanel.d.ts +14 -0
- package/dist/components/DraftPanel.d.ts.map +1 -0
- package/dist/components/ScribeConsentDialog.d.ts +12 -0
- package/dist/components/ScribeConsentDialog.d.ts.map +1 -0
- package/dist/components/ScribeRecordingPill.d.ts +16 -0
- package/dist/components/ScribeRecordingPill.d.ts.map +1 -0
- package/dist/components/ScribeRecoveryBanner.d.ts +8 -0
- package/dist/components/ScribeRecoveryBanner.d.ts.map +1 -0
- package/dist/components/ScribeSessionPage.d.ts +13 -0
- package/dist/components/ScribeSessionPage.d.ts.map +1 -0
- package/dist/components/ScribeShellMount.d.ts +19 -0
- package/dist/components/ScribeShellMount.d.ts.map +1 -0
- package/dist/components/TranscriptPanel.d.ts +10 -0
- package/dist/components/TranscriptPanel.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +63 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/tables.d.ts +31 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2910 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +19 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.test.d.ts +2 -0
- package/dist/lib/config.test.d.ts.map +1 -0
- package/dist/lib/drain.d.ts +13 -0
- package/dist/lib/drain.d.ts.map +1 -0
- package/dist/lib/generate.d.ts +23 -0
- package/dist/lib/generate.d.ts.map +1 -0
- package/dist/lib/prompt.d.ts +39 -0
- package/dist/lib/prompt.d.ts.map +1 -0
- package/dist/lib/prompt.test.d.ts +2 -0
- package/dist/lib/prompt.test.d.ts.map +1 -0
- package/dist/lib/transport.d.ts +59 -0
- package/dist/lib/transport.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +6 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/runtime/capture.d.ts +84 -0
- package/dist/runtime/capture.d.ts.map +1 -0
- package/dist/runtime/idb.d.ts +121 -0
- package/dist/runtime/idb.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +48 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/pump.d.ts +11 -0
- package/dist/runtime/pump.d.ts.map +1 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.d.ts.map +1 -0
- package/functions/scribe-generate/index.ts +255 -0
- package/functions/scribe-transcribe/index.ts +455 -0
- package/package.json +58 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ScribeSttConfig } from '../types';
|
|
2
|
+
/** A configuração pede o broker da plataforma? É o default. */
|
|
3
|
+
export declare function usesFayzTransport(stt: Pick<ScribeSttConfig, 'transport'>): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Lança se a configuração não descreve um caminho de transcrição próprio.
|
|
6
|
+
*
|
|
7
|
+
* Por que lançar na construção do plugin, e não degradar: sem transporte
|
|
8
|
+
* configurado a única alternativa a mão seria o reconhecedor nativo do browser,
|
|
9
|
+
* que envia o áudio do microfone para servidores do Google. Num atendimento
|
|
10
|
+
* isso é um operador não declarado processando dado sensível — sem DPA, sem
|
|
11
|
+
* base documentada de transferência internacional, e sem como honrar um pedido
|
|
12
|
+
* de eliminação, porque não dá para pedir a deleção do que você nunca soube que
|
|
13
|
+
* foi recebido.
|
|
14
|
+
*
|
|
15
|
+
* Falhar alto no boot é infinitamente melhor que degradar em silêncio para o
|
|
16
|
+
* caminho que ninguém escolheu.
|
|
17
|
+
*/
|
|
18
|
+
export declare function assertSttConfig(stt: ScribeSttConfig | undefined): asserts stt is ScribeSttConfig;
|
|
19
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAW/C,+DAA+D;AAC/D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,CAAC,GAAG,OAAO,CAElF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,SAAS,GAAG,OAAO,CAAC,GAAG,IAAI,eAAe,CAyBhG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../src/lib/config.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ScribeSttConfig } from '../types';
|
|
2
|
+
export interface DrainResult {
|
|
3
|
+
transcribed: number;
|
|
4
|
+
failed: number;
|
|
5
|
+
remaining: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Transcribes everything pending in a session. Returns `null` when the
|
|
9
|
+
* configured transport is not the browser's — the caller falls back to the edge
|
|
10
|
+
* function.
|
|
11
|
+
*/
|
|
12
|
+
export declare function drainSessionViaTransport(sessionId: string, stt: ScribeSttConfig): Promise<DrainResult | null>;
|
|
13
|
+
//# sourceMappingURL=drain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drain.d.ts","sourceRoot":"","sources":["../../src/lib/drain.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAgC/C,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAQD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA0D7B"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { NarrativeSchema, ScribeSttConfig } from '../types';
|
|
2
|
+
export interface GenerateResult {
|
|
3
|
+
generationId: string;
|
|
4
|
+
markdown: string;
|
|
5
|
+
model: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Gera um documento a partir da transcrição da sessão.
|
|
9
|
+
*
|
|
10
|
+
* Devolve `null` quando o transporte configurado não é o do browser — aí quem
|
|
11
|
+
* chama deve usar a edge function.
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateViaTransport(args: {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
schema: NarrativeSchema;
|
|
16
|
+
templateName: string;
|
|
17
|
+
templateId?: string;
|
|
18
|
+
templateKey?: string;
|
|
19
|
+
tabOrder?: number;
|
|
20
|
+
stt: ScribeSttConfig;
|
|
21
|
+
model?: string;
|
|
22
|
+
}): Promise<GenerateResult | null>;
|
|
23
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/lib/generate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AA4ChE,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,eAAe,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,eAAe,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAiGjC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { NarrativeSchema } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Versão do prompt, gravada em `plg_scribe_generations.prompt_version`. Subir
|
|
4
|
+
* este número a cada mudança de forma é o que torna possível responder "por que
|
|
5
|
+
* os documentos de março saíram diferentes dos de abril".
|
|
6
|
+
*/
|
|
7
|
+
export declare const PROMPT_VERSION = "scribe/narrative@1";
|
|
8
|
+
export interface TranscriptLine {
|
|
9
|
+
startOffsetMs: number;
|
|
10
|
+
text: string;
|
|
11
|
+
gap: boolean;
|
|
12
|
+
speaker?: number;
|
|
13
|
+
}
|
|
14
|
+
/** `mm:ss` — a marcação que o profissional usa para achar o trecho no áudio. */
|
|
15
|
+
export declare function formatOffset(ms: number): string;
|
|
16
|
+
/**
|
|
17
|
+
* Renderiza a transcrição para o modelo. Buracos entram como linha EXPLÍCITA:
|
|
18
|
+
* o modelo precisa saber que houve descontinuidade, senão ele costura os dois
|
|
19
|
+
* lados numa narrativa contínua e inventa a ponte.
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderTranscript(lines: TranscriptLine[]): string;
|
|
22
|
+
export declare function buildSystemPrompt(schema: NarrativeSchema): string;
|
|
23
|
+
export declare function buildUserPrompt(args: {
|
|
24
|
+
schema: NarrativeSchema;
|
|
25
|
+
transcript: string;
|
|
26
|
+
context?: Record<string, string | undefined>;
|
|
27
|
+
}): string;
|
|
28
|
+
/**
|
|
29
|
+
* Quebra o markdown de volta em seções por `id`, casando os títulos `## `. Serve
|
|
30
|
+
* para o commit gravar `data.sections` e para o assistente reescrever UMA seção
|
|
31
|
+
* sem tocar no resto.
|
|
32
|
+
*
|
|
33
|
+
* Casa por título normalizado porque é o único âncora que o modelo devolve —
|
|
34
|
+
* daí o `id` da seção nunca ser traduzido: ele é a chave, o título é a etiqueta.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseSections(markdown: string, schema: NarrativeSchema): Record<string, string>;
|
|
37
|
+
/** Título do documento: primeira seção com conteúdo, encurtada. Cai no nome do template. */
|
|
38
|
+
export declare function deriveTitle(markdown: string, fallback: string, maxLen?: number): string;
|
|
39
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/lib/prompt.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAuB,MAAM,UAAU,CAAA;AAEpE;;;;GAIG;AACH,eAAO,MAAM,cAAc,uBAAuB,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,OAAO,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,CAUhE;AAeD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAqCjE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,MAAM,EAAE,eAAe,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAC7C,GAAG,MAAM,CA4BT;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2B/F;AAED,4FAA4F;AAC5F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,MAAM,CAOnF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.test.d.ts","sourceRoot":"","sources":["../../src/lib/prompt.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ScribeSttConfig, ScribeTransportKind } from '../types';
|
|
2
|
+
export interface SttClipResult {
|
|
3
|
+
text: string;
|
|
4
|
+
words: Array<{
|
|
5
|
+
w: string;
|
|
6
|
+
s: number;
|
|
7
|
+
e: number;
|
|
8
|
+
c?: number;
|
|
9
|
+
sp?: number;
|
|
10
|
+
}>;
|
|
11
|
+
/** 0 when the provider does not report it — callers read 0 as unknown. */
|
|
12
|
+
durationMs: number;
|
|
13
|
+
confidence: number;
|
|
14
|
+
/** `provider:model`, so the stored row says what produced it. */
|
|
15
|
+
provider: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CompletionResult {
|
|
18
|
+
content: string;
|
|
19
|
+
model: string;
|
|
20
|
+
tokensIn?: number;
|
|
21
|
+
tokensOut?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ScribeTransport {
|
|
24
|
+
kind: ScribeTransportKind;
|
|
25
|
+
/**
|
|
26
|
+
* Transcribes ONE clip. The caller owns the queue, the retry and the state;
|
|
27
|
+
* the transport only knows how to talk to a model.
|
|
28
|
+
*/
|
|
29
|
+
transcribeClip(args: {
|
|
30
|
+
audio: Blob;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
locale: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
diarize?: boolean;
|
|
35
|
+
/** Tail of the previous clip; rescues the word cut at the seam. */
|
|
36
|
+
context?: string;
|
|
37
|
+
}): Promise<SttClipResult>;
|
|
38
|
+
/** One completion: no conversation, no history, nothing persisted. */
|
|
39
|
+
complete(args: {
|
|
40
|
+
system: string;
|
|
41
|
+
user: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
temperature?: number;
|
|
44
|
+
maxTokens?: number;
|
|
45
|
+
}): Promise<CompletionResult>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The transport actually in use, given what is configured AND what runtime
|
|
49
|
+
* offers.
|
|
50
|
+
*
|
|
51
|
+
* `transport: 'fayz'` with no resolved connection returns null rather than
|
|
52
|
+
* throwing: an app built outside the platform container has no `VITE_FAYZ_*`,
|
|
53
|
+
* and the right answer there is a screen saying transcription is unavailable —
|
|
54
|
+
* not the whole app failing to boot over a tab nobody opened. `'edge'` never
|
|
55
|
+
* reaches here.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveScribeTransport(stt: ScribeSttConfig): ScribeTransport | null;
|
|
58
|
+
export { usesFayzTransport } from './config';
|
|
59
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/lib/transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAyBpE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,KAAK,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1E,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAA;IACzB;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE;QACnB,KAAK,EAAE,IAAI,CAAA;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,EAAE,MAAM,CAAA;QACd,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,mEAAmE;QACnE,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE;QACb,MAAM,EAAE,MAAM,CAAA;QACd,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;CAC9B;AAmGD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,eAAe,GAAG,eAAe,GAAG,IAAI,CAGnF;AAED,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAYrC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/locales/index.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGhE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAYvC,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const MIGRATION_001_SCRIBE_BASE = "-- plugin-scribe 001: gravar um atendimento inteiro sem poder perd\u00EA-lo.\n--\n-- O problema que este schema resolve n\u00E3o \u00E9 \"guardar \u00E1udio\". \u00C9 que o profissional\n-- aperta gravar, navega o app por 40 minutos, e QUALQUER coisa pode acontecer no\n-- meio: reload, aba morta, wifi caindo, tampa fechando. Perder a consulta \u00E9 uma\n-- falha irrecuper\u00E1vel \u2014 n\u00E3o d\u00E1 para pedir para o paciente repetir.\n--\n-- Por isso o desenho n\u00E3o tem \"o arquivo da sess\u00E3o\". Tem um MANIFESTO: a sess\u00E3o\n-- \u00E9 uma linha, e cada 30 segundos de \u00E1udio \u00E9 OUTRA linha que sobe sozinha e \u00E9\n-- transcrita sozinha. O que chegou, chegou. O cliente pode morrer entre dois\n-- segmentos e o que j\u00E1 subiu continua \u00EDntegro e j\u00E1 transcrito.\n--\n-- ---------------------------------------------------------------------------\n-- Duas dura\u00E7\u00F5es, e elas discordam de prop\u00F3sito\n-- ---------------------------------------------------------------------------\n-- `wall_duration_ms` \u00E9 o rel\u00F3gio de parede que o browser reportou. Serve para o\n-- cron\u00F4metro na tela e para mais nada.\n--\n-- `audio_duration_ms` \u00E9 SUM(segments.duration_ms) \u2014 soma do \u00E1udio que de fato\n-- existe, com a dura\u00E7\u00E3o vinda da resposta do STT, n\u00E3o de um `setInterval`.\n--\n-- Elas divergem sempre que a aba congela: notebook dormindo por 10 minutos faz\n-- o rel\u00F3gio de parede contar a soneca inteira enquanto o MediaRecorder n\u00E3o\n-- emitiu um byte. Registro cl\u00EDnico, medidor de cota e fatura usam a SEGUNDA.\n-- Confundir as duas \u00E9 cobrar do cliente pelo cochilo do notebook.\n--\n-- ---------------------------------------------------------------------------\n-- O contador nunca \u00E9 escrito pelo browser\n-- ---------------------------------------------------------------------------\n-- `scribe_record_segment` (se\u00E7\u00E3o 5) faz o upsert do segmento e RECALCULA os\n-- contadores da sess\u00E3o a partir da tabela de segmentos. Um cliente que sobe o\n-- mesmo segmento duas vezes (replay depois de queda de rede) converge para o\n-- mesmo n\u00FAmero em vez de somar duas vezes.\n\n-- ---------------------------------------------------------------------------\n-- 1. sessions \u2014 o encontro\n-- ---------------------------------------------------------------------------\n-- `id` N\u00C3O tem DEFAULT gen_random_uuid(). \u00C9 gerado no cliente, antes do primeiro\n-- byte, e usado como chave no IndexedDB e no caminho do Storage. Sem isso a\n-- grava\u00E7\u00E3o teria que esperar um round-trip para saber onde escrever \u2014 e uma\n-- grava\u00E7\u00E3o que depende da rede para COME\u00C7AR j\u00E1 nasce fr\u00E1gil.\nCREATE TABLE IF NOT EXISTS public.plg_scribe_sessions (\n id uuid PRIMARY KEY,\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n\n -- O titular. SET NULL e n\u00E3o CASCADE: apagar a pessoa n\u00E3o pode apagar em\n -- sil\u00EAncio o registro do atendimento que aconteceu.\n subject_id uuid REFERENCES public.people(id) ON DELETE SET NULL,\n context_kind text NOT NULL DEFAULT 'person'\n CHECK (context_kind IN ('person', 'appointment', 'standalone')),\n appointment_id uuid REFERENCES public.appointments(id) ON DELETE SET NULL,\n owner_user_id uuid NOT NULL,\n\n status text NOT NULL DEFAULT 'recording'\n CHECK (status IN ('recording','paused','interrupted','uploading','transcribing',\n 'ready','generating','completed','abandoned','failed')),\n locale text NOT NULL DEFAULT 'pt-BR',\n mime_type text,\n\n started_at timestamptz NOT NULL DEFAULT now(),\n ended_at timestamptz,\n wall_duration_ms integer NOT NULL DEFAULT 0,\n audio_duration_ms integer NOT NULL DEFAULT 0,\n\n segment_count integer NOT NULL DEFAULT 0,\n uploaded_segment_count integer NOT NULL DEFAULT 0,\n transcribed_segment_count integer NOT NULL DEFAULT 0,\n transcript_chars integer NOT NULL DEFAULT 0,\n\n -- Base legal, carimbada antes do primeiro byte. \"Quando\" e \"como\" \u00E9 o que se\n -- prova depois; guardar isso na mesma linha da grava\u00E7\u00E3o \u00E9 o que torna a prova\n -- barata.\n consent_at timestamptz,\n consent_mode text CHECK (consent_mode IN ('verbal','written','implied_contract')),\n\n -- Reten\u00E7\u00E3o: o \u00E1udio some, a transcri\u00E7\u00E3o e o documento assinado ficam.\n retention_until date,\n audio_deleted_at timestamptz,\n\n error text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_scribe_sessions ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_scribe_sessions_tenant\n ON public.plg_scribe_sessions(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_scribe_sessions_subject\n ON public.plg_scribe_sessions(subject_id) WHERE subject_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_plg_scribe_sessions_appointment\n ON public.plg_scribe_sessions(appointment_id) WHERE appointment_id IS NOT NULL;\n\n-- A consulta da recupera\u00E7\u00E3o: \"este usu\u00E1rio tem alguma sess\u00E3o aberta?\". Roda em\n-- todo boot do app, ent\u00E3o \u00E9 \u00EDndice parcial e n\u00E3o varredura.\nCREATE INDEX IF NOT EXISTS idx_plg_scribe_sessions_open\n ON public.plg_scribe_sessions(tenant_id, owner_user_id, status)\n WHERE status IN ('recording','paused','interrupted','uploading','transcribing','ready','generating');\n\n-- A consulta do job de reten\u00E7\u00E3o.\nCREATE INDEX IF NOT EXISTS idx_plg_scribe_sessions_retention\n ON public.plg_scribe_sessions(retention_until)\n WHERE audio_deleted_at IS NULL AND retention_until IS NOT NULL;\n\n-- ---------------------------------------------------------------------------\n-- 2. segments \u2014 o manifesto dos peda\u00E7os\n-- ---------------------------------------------------------------------------\n-- A PK \u00E9 (session_id, seg_index) e n\u00E3o um uuid: o \u00EDndice do segmento j\u00E1 \u00E9\n-- \u00FAnico dentro da sess\u00E3o, e usar isso como chave faz o re-upload do mesmo\n-- peda\u00E7o ser um ON CONFLICT em vez de uma linha duplicada. Idempot\u00EAncia de\n-- gra\u00E7a, que \u00E9 exatamente o que um pump com retry infinito precisa.\nCREATE TABLE IF NOT EXISTS public.plg_scribe_segments (\n session_id uuid NOT NULL REFERENCES public.plg_scribe_sessions(id) ON DELETE CASCADE,\n seg_index integer NOT NULL,\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n\n storage_path text,\n bytes integer,\n start_offset_ms integer NOT NULL,\n -- Vem da resposta do STT. O rel\u00F3gio do JS n\u00E3o \u00E9 testemunha confi\u00E1vel de\n -- quanto \u00E1udio existe: ele conta tempo, n\u00E3o amostras.\n duration_ms integer,\n\n -- Montado a partir de pieces de um segmento cortado no meio (crash). Vale\n -- transcrever mesmo assim: 22 segundos de consulta \u00E9 melhor que zero.\n partial boolean NOT NULL DEFAULT false,\n\n -- Linha SINT\u00C9TICA: n\u00E3o tem \u00E1udio, marca um buraco (sono, mic perdido,\n -- permiss\u00E3o revogada). Existe para a transcri\u00E7\u00E3o ADMITIR o buraco em vez de\n -- emendar dois momentos como se fossem cont\u00EDnuos. Isso \u00E9 requisito\n -- m\u00E9dico-legal: um registro que esconde a costura \u00E9 pior que um com furo.\n gap boolean NOT NULL DEFAULT false,\n\n upload_state text NOT NULL DEFAULT 'pending'\n CHECK (upload_state IN ('pending','uploaded','missing')),\n uploaded_at timestamptz,\n\n stt_state text NOT NULL DEFAULT 'pending'\n CHECK (stt_state IN ('pending','running','done','failed','skipped')),\n stt_provider text,\n stt_attempts smallint NOT NULL DEFAULT 0,\n stt_error text,\n\n text text,\n -- [{w,s,e,c,sp}] com offsets RELATIVOS a este segmento. Absoluto se calcula\n -- somando start_offset_ms; guardar relativo \u00E9 o que deixa reprocessar um\n -- segmento sozinho sem reescrever os vizinhos.\n words jsonb,\n confidence real,\n\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (session_id, seg_index)\n);\nALTER TABLE public.plg_scribe_segments ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_scribe_segments_tenant\n ON public.plg_scribe_segments(tenant_id);\n\n-- A fila do STT. Parcial porque s\u00F3 interessa o que j\u00E1 subiu e ainda n\u00E3o foi\n-- transcrito \u2014 que \u00E9 uma fra\u00E7\u00E3o min\u00FAscula da tabela em regime permanente.\nCREATE INDEX IF NOT EXISTS idx_plg_scribe_segments_stt_queue\n ON public.plg_scribe_segments(tenant_id, stt_state, created_at)\n WHERE upload_state = 'uploaded' AND stt_state IN ('pending','failed');\n\n-- ---------------------------------------------------------------------------\n-- 3. generations \u2014 uma tentativa de documento\n-- ---------------------------------------------------------------------------\n-- DUAS colunas de markdown, e isso n\u00E3o \u00E9 redund\u00E2ncia.\n--\n-- `markdown` sa\u00EDda do modelo, escrita uma vez, nunca mutada\n-- `markdown_edited` a c\u00F3pia de trabalho do humano\n--\n-- A diferen\u00E7a entre elas \u00E9 a \u00FAnica prova de que algu\u00E9m revisou. Ela sustenta\n-- tr\u00EAs coisas: o selo \"revisado pelo profissional\", o diff que diz se o prompt\n-- est\u00E1 bom (documento muito editado = prompt ruim), e a cadeia de proveni\u00EAncia\n-- que um registro m\u00E9dico-legal precisa ter. Guardar s\u00F3 o texto final apaga tudo\n-- isso para economizar uma coluna de texto.\nCREATE TABLE IF NOT EXISTS public.plg_scribe_generations (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n session_id uuid NOT NULL REFERENCES public.plg_scribe_sessions(id) ON DELETE CASCADE,\n\n -- Um dos dois. `template_id` quando o tenant forkou e tem linha pr\u00F3pria;\n -- `template_key` quando \u00E9 preset de c\u00F3digo (que de prop\u00F3sito n\u00E3o tem linha,\n -- para n\u00E3o consumir a cota de templates do plano).\n template_id uuid,\n template_key text,\n\n tab_order integer NOT NULL DEFAULT 0,\n title text,\n status text NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending','running','ready','failed','committed','discarded')),\n\n markdown text,\n markdown_edited text,\n\n -- Extra\u00E7\u00E3o stage-1 do modo two-stage. A coluna entra agora, vazia, porque a\n -- interface j\u00E1 nasce com esse formato \u2014 quando as abas de multi-documento\n -- chegarem, \u00E9 preencher, n\u00E3o migrar.\n facts jsonb,\n\n model text,\n prompt_version text,\n input_tokens integer,\n output_tokens integer,\n error text,\n\n document_id uuid,\n created_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_scribe_generations ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_scribe_generations_session\n ON public.plg_scribe_generations(session_id, tab_order);\nCREATE INDEX IF NOT EXISTS idx_plg_scribe_generations_tenant\n ON public.plg_scribe_generations(tenant_id);\n\n-- FKs para plugin-forms e para o arqu\u00E9tipo de documentos s\u00E3o condicionais: o\n-- scribe declara `dependencies: ['custom_forms']`, mas a ordem de aplica\u00E7\u00E3o\n-- entre plugins n\u00E3o \u00E9 garantida e um pool pode aplicar este arquivo antes.\n-- Guardado, o re-apply converge; sem guarda, quebraria o passo inteiro.\nDO $$\nBEGIN\n IF to_regclass('public.plg_forms_templates') IS NOT NULL\n AND NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_scribe_generations_template_fk') THEN\n ALTER TABLE public.plg_scribe_generations\n ADD CONSTRAINT plg_scribe_generations_template_fk\n FOREIGN KEY (template_id) REFERENCES public.plg_forms_templates(id) ON DELETE SET NULL;\n END IF;\n\n IF to_regclass('public.documents') IS NOT NULL\n AND NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_scribe_generations_document_fk') THEN\n ALTER TABLE public.plg_scribe_generations\n ADD CONSTRAINT plg_scribe_generations_document_fk\n FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\n-- ---------------------------------------------------------------------------\n-- 4. RLS \u2014 a forma can\u00F4nica deste repo, tr\u00EAs tabelas x quatro verbos\n-- ---------------------------------------------------------------------------\nDROP POLICY IF EXISTS plg_scribe_sessions_select ON public.plg_scribe_sessions;\nCREATE POLICY plg_scribe_sessions_select ON public.plg_scribe_sessions FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS plg_scribe_sessions_insert ON public.plg_scribe_sessions;\nCREATE POLICY plg_scribe_sessions_insert ON public.plg_scribe_sessions FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS plg_scribe_sessions_update ON public.plg_scribe_sessions;\nCREATE POLICY plg_scribe_sessions_update ON public.plg_scribe_sessions FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS plg_scribe_sessions_delete ON public.plg_scribe_sessions;\nCREATE POLICY plg_scribe_sessions_delete ON public.plg_scribe_sessions FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_scribe_sessions TO authenticated;\n\nDROP POLICY IF EXISTS plg_scribe_segments_select ON public.plg_scribe_segments;\nCREATE POLICY plg_scribe_segments_select ON public.plg_scribe_segments FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS plg_scribe_segments_insert ON public.plg_scribe_segments;\nCREATE POLICY plg_scribe_segments_insert ON public.plg_scribe_segments FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS plg_scribe_segments_update ON public.plg_scribe_segments;\nCREATE POLICY plg_scribe_segments_update ON public.plg_scribe_segments FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS plg_scribe_segments_delete ON public.plg_scribe_segments;\nCREATE POLICY plg_scribe_segments_delete ON public.plg_scribe_segments FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_scribe_segments TO authenticated;\n\nDROP POLICY IF EXISTS plg_scribe_generations_select ON public.plg_scribe_generations;\nCREATE POLICY plg_scribe_generations_select ON public.plg_scribe_generations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS plg_scribe_generations_insert ON public.plg_scribe_generations;\nCREATE POLICY plg_scribe_generations_insert ON public.plg_scribe_generations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS plg_scribe_generations_update ON public.plg_scribe_generations;\nCREATE POLICY plg_scribe_generations_update ON public.plg_scribe_generations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS plg_scribe_generations_delete ON public.plg_scribe_generations;\nCREATE POLICY plg_scribe_generations_delete ON public.plg_scribe_generations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_scribe_generations TO authenticated;\n\n-- ---------------------------------------------------------------------------\n-- 5. scribe_record_segment \u2014 o browser registra, o banco conta\n-- ---------------------------------------------------------------------------\n-- SECURITY INVOKER: a RLS acima j\u00E1 amarra o tenant. N\u00E3o h\u00E1 nada aqui que\n-- precise de privil\u00E9gio elevado, e uma fun\u00E7\u00E3o SECURITY DEFINER que n\u00E3o precisa\n-- ser \u00E9 s\u00F3 superf\u00EDcie de ataque a mais.\n--\n-- O ponto da fun\u00E7\u00E3o \u00E9 a segunda metade: recalcular os contadores da sess\u00E3o a\n-- partir de um SELECT sobre os segmentos, nunca de um `+1` vindo do cliente.\n-- Um pump com retry infinito vai reenviar o mesmo segmento, e um contador\n-- incremental transformaria isso em n\u00FAmeros que crescem sem o \u00E1udio crescer.\nCREATE OR REPLACE FUNCTION public.scribe_record_segment(p_payload jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY INVOKER\nSET search_path TO 'public'\nAS $function$\nDECLARE\n v_session_id uuid;\n v_seg_index integer;\n v_tenant_id uuid;\n v_result jsonb;\nBEGIN\n v_session_id := (p_payload->>'session_id')::uuid;\n v_seg_index := (p_payload->>'seg_index')::integer;\n\n IF v_session_id IS NULL OR v_seg_index IS NULL THEN\n RAISE EXCEPTION 'session_id and seg_index are required';\n END IF;\n\n -- O tenant vem da SESS\u00C3O, nunca do payload. Se o cliente pudesse informar o\n -- tenant, ele escolheria em que pasta escrever.\n SELECT s.tenant_id INTO v_tenant_id\n FROM plg_scribe_sessions s WHERE s.id = v_session_id;\n\n IF v_tenant_id IS NULL THEN\n -- Sess\u00E3o inexistente OU invis\u00EDvel pela RLS: a mesma resposta nos dois\n -- casos, de prop\u00F3sito \u2014 n\u00E3o vaza a exist\u00EAncia de sess\u00E3o de outro tenant.\n RAISE EXCEPTION 'unknown session';\n END IF;\n\n INSERT INTO plg_scribe_segments (\n session_id, seg_index, tenant_id, storage_path, bytes,\n start_offset_ms, duration_ms, partial, gap, upload_state, uploaded_at, stt_state\n ) VALUES (\n v_session_id, v_seg_index, v_tenant_id,\n p_payload->>'storage_path',\n (p_payload->>'bytes')::integer,\n COALESCE((p_payload->>'start_offset_ms')::integer, 0),\n (p_payload->>'duration_ms')::integer,\n COALESCE((p_payload->>'partial')::boolean, false),\n COALESCE((p_payload->>'gap')::boolean, false),\n CASE WHEN COALESCE((p_payload->>'gap')::boolean, false) THEN 'missing' ELSE 'uploaded' END,\n now(),\n -- Buraco n\u00E3o vai para o STT: n\u00E3o h\u00E1 \u00E1udio para transcrever, e mand\u00E1-lo\n -- queimaria uma chamada para receber string vazia.\n CASE WHEN COALESCE((p_payload->>'gap')::boolean, false) THEN 'skipped' ELSE 'pending' END\n )\n ON CONFLICT (session_id, seg_index) DO UPDATE SET\n storage_path = COALESCE(EXCLUDED.storage_path, plg_scribe_segments.storage_path),\n bytes = COALESCE(EXCLUDED.bytes, plg_scribe_segments.bytes),\n duration_ms = COALESCE(EXCLUDED.duration_ms, plg_scribe_segments.duration_ms),\n partial = EXCLUDED.partial,\n upload_state = EXCLUDED.upload_state,\n uploaded_at = COALESCE(plg_scribe_segments.uploaded_at, EXCLUDED.uploaded_at),\n updated_at = now();\n\n -- Contadores derivados. `audio_duration_ms` soma s\u00F3 o que tem dura\u00E7\u00E3o medida,\n -- ent\u00E3o um segmento que ainda n\u00E3o voltou do STT n\u00E3o infla o total.\n UPDATE plg_scribe_sessions s SET\n segment_count = agg.total,\n uploaded_segment_count = agg.uploaded,\n transcribed_segment_count = agg.transcribed,\n audio_duration_ms = agg.audio_ms,\n transcript_chars = agg.chars,\n updated_at = now()\n FROM (\n SELECT\n count(*)::int AS total,\n count(*) FILTER (WHERE upload_state = 'uploaded')::int AS uploaded,\n count(*) FILTER (WHERE stt_state = 'done')::int AS transcribed,\n COALESCE(sum(duration_ms) FILTER (WHERE NOT gap), 0)::int AS audio_ms,\n COALESCE(sum(length(text)), 0)::int AS chars\n FROM plg_scribe_segments WHERE session_id = v_session_id\n ) agg\n WHERE s.id = v_session_id;\n\n SELECT to_jsonb(x) INTO v_result FROM (\n SELECT s.segment_count, s.uploaded_segment_count,\n s.transcribed_segment_count, s.audio_duration_ms, s.status\n FROM plg_scribe_sessions s WHERE s.id = v_session_id\n ) x;\n\n RETURN v_result;\nEND;\n$function$;\n\nCOMMENT ON FUNCTION public.scribe_record_segment(jsonb) IS\n 'Idempotent upsert of one audio segment + recomputation of the parent session counters from the segments table. Called by the browser upload pump after each successful Storage write.';\n\nGRANT EXECUTE ON FUNCTION public.scribe_record_segment(jsonb) TO authenticated;\n\n-- ---------------------------------------------------------------------------\n-- 6. o bucket\n-- ---------------------------------------------------------------------------\n-- PRIVADO, sem discuss\u00E3o: \u00E9 a voz de uma pessoa falando da pr\u00F3pria sa\u00FAde. URL\n-- p\u00FAblica seria um endere\u00E7o eterno para isso. O backoffice l\u00EA por URL assinada\n-- de curta dura\u00E7\u00E3o; a edge function de transcri\u00E7\u00E3o l\u00EA com service key.\n--\n-- 5 MB por SEGMENTO \u00E9 folgado por design: 30 s de Opus mono a 24 kbps d\u00E1 ~90 KB.\n-- O limite existe para barrar um cliente com bug mandando um arquivo inteiro,\n-- n\u00E3o para apertar o caso normal.\nINSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)\nVALUES (\n 'scribe-audio',\n 'scribe-audio',\n false,\n 5242880,\n ARRAY['audio/webm','audio/ogg','audio/mp4','audio/mpeg','audio/m4a','audio/x-m4a']\n)\nON CONFLICT (id) DO UPDATE SET\n public = EXCLUDED.public,\n file_size_limit = EXCLUDED.file_size_limit,\n allowed_mime_types = EXCLUDED.allowed_mime_types;\n\n-- ---------------------------------------------------------------------------\n-- 7. o caminho \u00E9 v\u00E1lido?\n-- ---------------------------------------------------------------------------\n-- Gram\u00E1tica (o SDK constr\u00F3i, o banco confere):\n--\n-- {tenant_id}/{yyyy}/{mm}/{session_id}/{seg_index:05d}.{ext}\n-- 1 2 3 4 5\n--\n-- 1 tenant_id chave do isolamento \u2014 toda policy pendura aqui\n-- 2,3 yyyy/mm varredura de reten\u00E7\u00E3o por prefixo (LGPD), pasta list\u00E1vel\n-- 4 session_id uuid do cliente: agrupa a sess\u00E3o e \u00E9 imprevis\u00EDvel\n-- 5 seg_index zero-padded, ent\u00E3o list() j\u00E1 volta em ordem cronol\u00F3gica\n--\n-- SECURITY DEFINER porque o EXISTS olha `tenants`, que tem RLS.\nCREATE OR REPLACE FUNCTION public.scribe_audio_path_ok(p_name text)\nRETURNS boolean\nLANGUAGE sql\nSTABLE\nSECURITY DEFINER\nSET search_path TO 'public'\nAS $function$\n SELECT\n p_name IS NOT NULL\n AND length(p_name) <= 200\n AND p_name ~ ('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'\n || '/[0-9]{4}/[0-9]{2}'\n || '/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'\n || '/[0-9]{5}\\.(webm|ogg|m4a|mp3)$')\n -- compara\u00E7\u00E3o em texto (nunca `::uuid`): entrada malformada devolve falso em\n -- vez de derrubar a policy com erro de cast.\n AND EXISTS (\n SELECT 1 FROM public.tenants t WHERE t.id::text = split_part(p_name, '/', 1)\n );\n$function$;\n\nCOMMENT ON FUNCTION public.scribe_audio_path_ok(text) IS\n 'True when a storage object name follows the scribe-audio grammar {tenant}/{yyyy}/{mm}/{session}/{segIndex}.{ext} and segment 1 is a real tenant. Used by the INSERT policy on that bucket.';\n\nGRANT EXECUTE ON FUNCTION public.scribe_audio_path_ok(text) TO authenticated;\n\n-- ---------------------------------------------------------------------------\n-- 8. pol\u00EDticas do bucket\n-- ---------------------------------------------------------------------------\nDROP POLICY IF EXISTS plg_scribe_audio_insert ON storage.objects;\nDROP POLICY IF EXISTS plg_scribe_audio_read ON storage.objects;\nDROP POLICY IF EXISTS plg_scribe_audio_delete ON storage.objects;\n\n-- Escrever: s\u00F3 logado (diferente do bucket do CRM, que aceita visitante an\u00F4nimo\n-- \u2014 aqui quem grava \u00E9 sempre um profissional autenticado), dentro da gram\u00E1tica\n-- e da pr\u00F3pria pasta.\n--\n-- N\u00C3O existe policy de UPDATE, e isso \u00E9 a trava mais importante das tr\u00EAs: sem\n-- ela o `upsert` falha, e um caminho j\u00E1 gravado NUNCA pode ser trocado por\n-- outro conte\u00FAdo. \u00C9 o que torna o \u00E1udio uma testemunha e n\u00E3o um rascunho.\nCREATE POLICY plg_scribe_audio_insert ON storage.objects\n FOR INSERT TO authenticated\n WITH CHECK (\n bucket_id = 'scribe-audio'\n AND public.scribe_audio_path_ok(name)\n AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)\n );\n\nCREATE POLICY plg_scribe_audio_read ON storage.objects\n FOR SELECT TO authenticated\n USING (\n bucket_id = 'scribe-audio'\n AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)\n );\n\n-- Apagar: mesma regra da leitura. \u00C9 o que torna \"apagar o \u00E1udio desta sess\u00E3o\"\n-- execut\u00E1vel por quem \u00E9 dono dele \u2014 o direito de elimina\u00E7\u00E3o do art. 18 VI n\u00E3o\n-- vale nada se ningu\u00E9m no tenant consegue exerc\u00EA-lo.\nCREATE POLICY plg_scribe_audio_delete ON storage.objects\n FOR DELETE TO authenticated\n USING (\n bucket_id = 'scribe-audio'\n AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)\n );\n";
|
|
2
|
+
export declare const MIGRATIONS: Array<{
|
|
3
|
+
id: string;
|
|
4
|
+
sql: string;
|
|
5
|
+
}>;
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,yBAAyB,kxyBA6drC,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAEzD,CAAA"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CaptureState, ConsentMode } from '../types';
|
|
2
|
+
import { type IdbSession } from './idb';
|
|
3
|
+
/** Unidade de durabilidade: no máximo isto de áudio vive só em RAM. */
|
|
4
|
+
export declare const PIECE_MS = 5000;
|
|
5
|
+
/** Unidade de upload + STT. 6 pieces por segmento. */
|
|
6
|
+
export declare const SEGMENT_MS = 30000;
|
|
7
|
+
export declare function pickMimeType(): string | null;
|
|
8
|
+
export declare function isCaptureSupported(): boolean;
|
|
9
|
+
type Listener = (snapshot: CaptureSnapshot) => void;
|
|
10
|
+
export interface CaptureSnapshot {
|
|
11
|
+
state: CaptureState;
|
|
12
|
+
sessionId: string | null;
|
|
13
|
+
subjectId?: string;
|
|
14
|
+
subjectName?: string;
|
|
15
|
+
/** Relógio de parede. Move o cronômetro; NÃO é o registro. */
|
|
16
|
+
elapsedMs: number;
|
|
17
|
+
segIndex: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function getSnapshot(): CaptureSnapshot;
|
|
21
|
+
export declare function subscribeCapture(fn: Listener): () => void;
|
|
22
|
+
/**
|
|
23
|
+
* Timers throttlam. Uma aba em segundo plano clampa `setInterval` em ≥1/s, e
|
|
24
|
+
* uma aba congelada não roda nada. Qualquer cronômetro que ACUMULE ticks vai
|
|
25
|
+
* mentir — e mentir para menos, que é o modo de falha silencioso.
|
|
26
|
+
*
|
|
27
|
+
* `Date.now()` sobrevive a throttle e a sono do sistema operacional. O preço é
|
|
28
|
+
* ficar sujeito a salto de relógio de parede (NTP, fuso), o que é raro e
|
|
29
|
+
* detectável, enquanto o throttle é garantido e invisível.
|
|
30
|
+
*
|
|
31
|
+
* Nada disso é o registro: a duração que vale é SUM(segments.duration_ms), do
|
|
32
|
+
* áudio que de fato existe. Este número move o pill e para por aí.
|
|
33
|
+
*/
|
|
34
|
+
export declare function elapsedMs(session: IdbSession): number;
|
|
35
|
+
export interface StartOptions {
|
|
36
|
+
tenantId: string;
|
|
37
|
+
userId: string;
|
|
38
|
+
subjectId?: string;
|
|
39
|
+
subjectName?: string;
|
|
40
|
+
appointmentId?: string;
|
|
41
|
+
locale?: string;
|
|
42
|
+
templateKeys?: string[];
|
|
43
|
+
consentMode?: ConsentMode;
|
|
44
|
+
/** Chamado assim que a linha do servidor deve existir. */
|
|
45
|
+
onSessionCreated?: (session: IdbSession, consentMode?: ConsentMode) => Promise<void> | void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Começa a gravar. A ordem aqui importa: checagem de suporte → saúde do
|
|
49
|
+
* armazenamento → permissão do microfone → linha local → PRIMEIRO BYTE. A linha
|
|
50
|
+
* no servidor é notificada depois, porque uma gravação que depende da rede para
|
|
51
|
+
* COMEÇAR já nasce frágil — e o id é gerado aqui, então não há round-trip.
|
|
52
|
+
*/
|
|
53
|
+
export declare function startCapture(opts: StartOptions): Promise<{
|
|
54
|
+
ok: true;
|
|
55
|
+
sessionId: string;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: string;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Pausa. A TRACK CONTINUA VIVA de propósito: soltar o stream e chamar
|
|
62
|
+
* `getUserMedia` de novo no resume arrisca um segundo prompt de permissão no
|
|
63
|
+
* meio da consulta, e um prompt no meio de uma consulta é uma gravação perdida.
|
|
64
|
+
*/
|
|
65
|
+
export declare function pauseCapture(): Promise<void>;
|
|
66
|
+
export declare function resumeCapture(): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Encerra. Devolve o id para a tela navegar para a sessão. O upload NÃO espera
|
|
69
|
+
* por isto — o pump já subiu quase tudo durante a gravação; o que sobra aqui é
|
|
70
|
+
* o último segmento.
|
|
71
|
+
*/
|
|
72
|
+
export declare function stopCapture(): Promise<string | null>;
|
|
73
|
+
/**
|
|
74
|
+
* Reata a UI a uma sessão órfã sem religar o microfone. Um browser não permite
|
|
75
|
+
* reabrir o mic sem gesto do usuário depois de um reload, então "retomar" é
|
|
76
|
+
* sempre um clique — o que esta função faz é deixar o pill e o banner
|
|
77
|
+
* enxergarem a sessão de novo.
|
|
78
|
+
*/
|
|
79
|
+
export declare function adoptOrphan(session: IdbSession): Promise<void>;
|
|
80
|
+
/** Descarta a sessão viva sem apagar nada — o servidor decide o que fazer com o áudio. */
|
|
81
|
+
export declare function releaseCapture(): Promise<void>;
|
|
82
|
+
export declare function getLiveSessionId(): string | null;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/runtime/capture.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAQL,KAAK,UAAU,EAChB,MAAM,OAAO,CAAA;AAEd,uEAAuE;AACvE,eAAO,MAAM,QAAQ,OAAQ,CAAA;AAC7B,sDAAsD;AACtD,eAAO,MAAM,UAAU,QAAS,CAAA;AAYhC,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAU5C;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAO5C;AA2BD,KAAK,QAAQ,GAAG,CAAC,QAAQ,EAAE,eAAe,KAAK,IAAI,CAAA;AAGnD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,YAAY,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAID,wBAAgB,WAAW,IAAI,eAAe,CAW7C;AAaD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,IAAI,CAIzD;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAGrD;AAmPD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0DAA0D;IAC1D,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC5F;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA2F/H;AAED;;;;GAIG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAelD;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAkCnD;AAED;;;;GAIG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2C1D;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBpE;AAED,0FAA0F;AAC1F,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAUpD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { CaptureState } from '../types';
|
|
2
|
+
export declare const STORE_SESSIONS = "sessions";
|
|
3
|
+
export declare const STORE_PIECES = "pieces";
|
|
4
|
+
export declare const STORE_SEGMENTS = "segments";
|
|
5
|
+
export declare const STORE_TRANSCRIPT = "transcript";
|
|
6
|
+
/** Índice usado pelo pump para achar o que ainda precisa subir. */
|
|
7
|
+
export declare const IDX_SEGMENT_UPLOAD = "by_upload";
|
|
8
|
+
export interface IdbSession {
|
|
9
|
+
/** uuid gerado no cliente. É o mesmo id da linha no servidor. */
|
|
10
|
+
id: string;
|
|
11
|
+
tenantId: string;
|
|
12
|
+
subjectId?: string;
|
|
13
|
+
subjectName?: string;
|
|
14
|
+
appointmentId?: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
locale: string;
|
|
18
|
+
startedAtEpochMs: number;
|
|
19
|
+
/** null enquanto pausado. Ver `elapsedMs` — o tempo é calculado, nunca contado. */
|
|
20
|
+
resumedAtEpochMs: number | null;
|
|
21
|
+
/** Relógio de parede acumulado através de pausas. */
|
|
22
|
+
accumulatedMs: number;
|
|
23
|
+
/** Heartbeat de 2 s. Sessão com tick velho no boot = órfã de um crash. */
|
|
24
|
+
lastTickEpochMs: number;
|
|
25
|
+
state: CaptureState;
|
|
26
|
+
/** Próximo segmento a abrir. */
|
|
27
|
+
segIndex: number;
|
|
28
|
+
templateKeys: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface IdbPiece {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
segIndex: number;
|
|
33
|
+
pieceIndex: number;
|
|
34
|
+
blob: Blob;
|
|
35
|
+
bytes: number;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
}
|
|
38
|
+
export interface IdbSegment {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
segIndex: number;
|
|
41
|
+
/** Vira null depois do upload — é o que devolve os bytes ao disco do usuário. */
|
|
42
|
+
blob: Blob | null;
|
|
43
|
+
bytes: number;
|
|
44
|
+
startOffsetMs: number;
|
|
45
|
+
/** Duração NOMINAL (contada pelo cliente). A real vem do STT, no servidor. */
|
|
46
|
+
nominalDurationMs: number;
|
|
47
|
+
partial: boolean;
|
|
48
|
+
gap: boolean;
|
|
49
|
+
uploadState: 'pending' | 'uploading' | 'uploaded' | 'failed';
|
|
50
|
+
attempts: number;
|
|
51
|
+
nextAttemptAt: number;
|
|
52
|
+
storagePath: string | null;
|
|
53
|
+
}
|
|
54
|
+
export interface IdbTranscriptPart {
|
|
55
|
+
sessionId: string;
|
|
56
|
+
segIndex: number;
|
|
57
|
+
text: string;
|
|
58
|
+
startOffsetMs: number;
|
|
59
|
+
endOffsetMs: number;
|
|
60
|
+
provider: string;
|
|
61
|
+
confidence: number;
|
|
62
|
+
}
|
|
63
|
+
export declare function isIdbAvailable(): boolean;
|
|
64
|
+
export declare function openScribeDb(): Promise<IDBDatabase>;
|
|
65
|
+
export declare function putSession(session: IdbSession): Promise<IDBValidKey>;
|
|
66
|
+
export declare function getSession(id: string): Promise<IdbSession | undefined>;
|
|
67
|
+
export declare function getAllSessions(): Promise<IdbSession[]>;
|
|
68
|
+
export declare function deleteSession(id: string): Promise<undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* Sessões que o boot precisa olhar: estavam vivas e o heartbeat morreu. Só
|
|
71
|
+
* `lastTickEpochMs` distingue "a aba fechou agora" de "isto está órfão desde
|
|
72
|
+
* ontem" — o estado sozinho não distingue, porque a aba que morreu não teve
|
|
73
|
+
* chance de escrever o estado final.
|
|
74
|
+
*/
|
|
75
|
+
export declare function findOrphanSessions(staleAfterMs?: number): Promise<IdbSession[]>;
|
|
76
|
+
export declare function putPiece(piece: IdbPiece): Promise<IDBValidKey>;
|
|
77
|
+
/** Todos os pieces de um segmento, em ordem de pieceIndex. */
|
|
78
|
+
export declare function getPieces(sessionId: string, segIndex: number): Promise<IdbPiece[]>;
|
|
79
|
+
/** Pieces soltos de uma sessão inteira — o que a recuperação encontra depois de um crash. */
|
|
80
|
+
export declare function getAllPiecesForSession(sessionId: string): Promise<IdbPiece[]>;
|
|
81
|
+
export declare function deletePieces(sessionId: string, segIndex: number): Promise<void>;
|
|
82
|
+
export declare function putSegment(segment: IdbSegment): Promise<IDBValidKey>;
|
|
83
|
+
export declare function getSegment(sessionId: string, segIndex: number): Promise<IdbSegment | undefined>;
|
|
84
|
+
export declare function getSegmentsForSession(sessionId: string): Promise<IdbSegment[]>;
|
|
85
|
+
/**
|
|
86
|
+
* O que o pump deve tentar agora: pendente ou falho, com `nextAttemptAt` já
|
|
87
|
+
* vencido. `uploading` fica de fora — se uma volta anterior travou nesse estado,
|
|
88
|
+
* o `reclaimStuckUploads` do boot é quem devolve, não este método.
|
|
89
|
+
*/
|
|
90
|
+
export declare function claimUploadable(limit: number): Promise<IdbSegment[]>;
|
|
91
|
+
/**
|
|
92
|
+
* Segmentos parados em `uploading` de uma aba que morreu no meio do upload.
|
|
93
|
+
* Sem isso eles ficariam invisíveis para sempre: `claimUploadable` só olha
|
|
94
|
+
* `pending` e `failed`.
|
|
95
|
+
*/
|
|
96
|
+
export declare function reclaimStuckUploads(): Promise<number>;
|
|
97
|
+
export declare function deleteSegmentsForSession(sessionId: string): Promise<void>;
|
|
98
|
+
export declare function putTranscriptPart(part: IdbTranscriptPart): Promise<IDBValidKey>;
|
|
99
|
+
export declare function getTranscriptForSession(sessionId: string): Promise<IdbTranscriptPart[]>;
|
|
100
|
+
/** Consulta de 40 min a 24 kbps mono ≈ 7,2 MB. 50 MB de folga cobre um dia ruim. */
|
|
101
|
+
export declare const MIN_FREE_BYTES: number;
|
|
102
|
+
export interface StorageHealth {
|
|
103
|
+
ok: boolean;
|
|
104
|
+
persisted: boolean;
|
|
105
|
+
freeBytes: number | null;
|
|
106
|
+
reason?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Pré-voo antes do primeiro byte. Duas coisas distintas:
|
|
110
|
+
*
|
|
111
|
+
* `persist()` pede ao browser para NÃO despejar este origin quando o disco
|
|
112
|
+
* apertar. Sem isso o navegador pode limpar o IndexedDB no meio da consulta e a
|
|
113
|
+
* gravação some sem erro nenhum — o pior modo de falha possível.
|
|
114
|
+
*
|
|
115
|
+
* `estimate()` é a checagem de espaço. Recusar a gravar com o disco cheio é
|
|
116
|
+
* infinitamente melhor que gravar 12 minutos e descobrir no minuto 13.
|
|
117
|
+
*/
|
|
118
|
+
export declare function checkStorageHealth(): Promise<StorageHealth>;
|
|
119
|
+
/** Só para testes — força a próxima chamada a reabrir o banco. */
|
|
120
|
+
export declare function __resetDbForTests(): void;
|
|
121
|
+
//# sourceMappingURL=idb.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idb.d.ts","sourceRoot":"","sources":["../../src/runtime/idb.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type IdbSession } from './idb';
|
|
2
|
+
import { elapsedMs, getSnapshot, getLiveSessionId, subscribeCapture } from './capture';
|
|
3
|
+
import type { ConsentMode, ScribeRetentionConfig } from '../types';
|
|
4
|
+
export * from './idb';
|
|
5
|
+
export * from './capture';
|
|
6
|
+
export { startPump, stopPump, kickPump, isPumpLeader, finalizeSession } from './pump';
|
|
7
|
+
export declare function subscribeOrphans(fn: (list: IdbSession[]) => void): () => void;
|
|
8
|
+
export interface ScribeRuntimeOptions {
|
|
9
|
+
retention?: Partial<ScribeRetentionConfig>;
|
|
10
|
+
}
|
|
11
|
+
export declare function initScribeRuntime(options?: ScribeRuntimeOptions): void;
|
|
12
|
+
export interface BeginSessionArgs {
|
|
13
|
+
subjectId?: string;
|
|
14
|
+
subjectName?: string;
|
|
15
|
+
appointmentId?: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
locale?: string;
|
|
18
|
+
templateKeys?: string[];
|
|
19
|
+
consentMode?: ConsentMode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Começa uma sessão: grava primeiro, avisa o servidor depois. Se o INSERT
|
|
23
|
+
* falhar (offline), a gravação segue — o áudio está no IndexedDB e o pump
|
|
24
|
+
* reconcilia. O caso contrário, exigir rede para começar, transformaria uma
|
|
25
|
+
* queda de wifi em consulta perdida.
|
|
26
|
+
*/
|
|
27
|
+
export declare function beginSession(args: BeginSessionArgs): Promise<{
|
|
28
|
+
ok: true;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: string;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function pauseSession(): Promise<void>;
|
|
35
|
+
export declare function resumeSession(): Promise<void>;
|
|
36
|
+
/** Encerra e devolve o id, para a tela navegar até a sessão. */
|
|
37
|
+
export declare function endSession(): Promise<string | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Descarte SUAVE. `abandoned`, não delete: o áudio fica até `retention_until`.
|
|
40
|
+
* Hard-delete de sessão de saúde por clique errado é um dano que não tem
|
|
41
|
+
* desfazer, e "descartar" quase sempre significa "não quero o documento", não
|
|
42
|
+
* "apague a consulta".
|
|
43
|
+
*/
|
|
44
|
+
export declare function discardSession(sessionId?: string): Promise<void>;
|
|
45
|
+
/** Dispensa o banner sem mexer no estado da sessão — ela continua no servidor. */
|
|
46
|
+
export declare function dismissOrphan(sessionId: string): void;
|
|
47
|
+
export { subscribeCapture, getSnapshot, elapsedMs, getLiveSessionId };
|
|
48
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,OAAO,CAAA;AACd,OAAO,EAEL,SAAS,EACT,WAAW,EACX,gBAAgB,EAMhB,gBAAgB,EAEjB,MAAM,WAAW,CAAA;AAElB,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAElE,cAAc,OAAO,CAAA;AACrB,cAAc,WAAW,CAAA;AACzB,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAA;AAarF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAI7E;AAYD,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAA;CAC3C;AAED,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAyBtE;AAMD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAwC1E;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAMlD;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAInD;AAED,gEAAgE;AAChE,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAUzD;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYtE;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGrD;AAgBD,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,EAAE,gBAAgB,EAAE,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Acorda o pump agora, sem esperar o poll. Chamado ao fechar um segmento. */
|
|
2
|
+
export declare function kickPump(): void;
|
|
3
|
+
export declare function startPump(): void;
|
|
4
|
+
export declare function stopPump(): void;
|
|
5
|
+
export declare function isPumpLeader(): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Marca a sessão como encerrada no servidor. Separado do `stopCapture` porque a
|
|
8
|
+
* captura não pode depender da rede para terminar.
|
|
9
|
+
*/
|
|
10
|
+
export declare function finalizeSession(sessionId: string, wallDurationMs: number): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=pump.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pump.d.ts","sourceRoot":"","sources":["../../src/runtime/pump.ts"],"names":[],"mappings":"AA8NA,8EAA8E;AAC9E,wBAAgB,QAAQ,IAAI,IAAI,CAI/B;AAED,wBAAgB,SAAS,IAAI,IAAI,CAQhC;AAED,wBAAgB,QAAQ,IAAI,IAAI,CAS/B;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CaptureState } from './types';
|
|
2
|
+
import type { IdbSession } from './runtime/idb';
|
|
3
|
+
export interface ScribeUiState {
|
|
4
|
+
state: CaptureState;
|
|
5
|
+
sessionId: string | null;
|
|
6
|
+
subjectId?: string;
|
|
7
|
+
subjectName?: string;
|
|
8
|
+
elapsedMs: number;
|
|
9
|
+
segIndex: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
/** Sessões interrompidas encontradas no boot. Alimentam o banner. */
|
|
12
|
+
orphans: IdbSession[];
|
|
13
|
+
/** Sessão cuja tela está aberta — não necessariamente a que está gravando. */
|
|
14
|
+
viewingSessionId: string | null;
|
|
15
|
+
setViewing: (id: string | null) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare const useScribeStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ScribeUiState>>;
|
|
18
|
+
/**
|
|
19
|
+
* Liga a store aos assinantes do runtime. Idempotente: a factory do plugin
|
|
20
|
+
* chama uma vez, e chamar de novo (HMR) não duplica assinatura.
|
|
21
|
+
*/
|
|
22
|
+
export declare function wireScribeStore(): void;
|
|
23
|
+
/** `mm:ss`, ou `h:mm:ss` quando passa da hora. */
|
|
24
|
+
export declare function formatElapsed(ms: number): string;
|
|
25
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAG/C,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,YAAY,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qEAAqE;IACrE,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CACxC;AAED,eAAO,MAAM,cAAc,4EAKxB,CAAA;AAIH;;;GAGG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAetC;AAED,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAQhD"}
|