@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/DraftPanel.d.ts +14 -0
  3. package/dist/components/DraftPanel.d.ts.map +1 -0
  4. package/dist/components/ScribeConsentDialog.d.ts +12 -0
  5. package/dist/components/ScribeConsentDialog.d.ts.map +1 -0
  6. package/dist/components/ScribeRecordingPill.d.ts +16 -0
  7. package/dist/components/ScribeRecordingPill.d.ts.map +1 -0
  8. package/dist/components/ScribeRecoveryBanner.d.ts +8 -0
  9. package/dist/components/ScribeRecoveryBanner.d.ts.map +1 -0
  10. package/dist/components/ScribeSessionPage.d.ts +13 -0
  11. package/dist/components/ScribeSessionPage.d.ts.map +1 -0
  12. package/dist/components/ScribeShellMount.d.ts +19 -0
  13. package/dist/components/ScribeShellMount.d.ts.map +1 -0
  14. package/dist/components/TranscriptPanel.d.ts +10 -0
  15. package/dist/components/TranscriptPanel.d.ts.map +1 -0
  16. package/dist/data/supabase.d.ts +63 -0
  17. package/dist/data/supabase.d.ts.map +1 -0
  18. package/dist/data/tables.d.ts +31 -0
  19. package/dist/data/tables.d.ts.map +1 -0
  20. package/dist/index.d.ts +13 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +2910 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/lib/config.d.ts +19 -0
  25. package/dist/lib/config.d.ts.map +1 -0
  26. package/dist/lib/config.test.d.ts +2 -0
  27. package/dist/lib/config.test.d.ts.map +1 -0
  28. package/dist/lib/drain.d.ts +13 -0
  29. package/dist/lib/drain.d.ts.map +1 -0
  30. package/dist/lib/generate.d.ts +23 -0
  31. package/dist/lib/generate.d.ts.map +1 -0
  32. package/dist/lib/prompt.d.ts +39 -0
  33. package/dist/lib/prompt.d.ts.map +1 -0
  34. package/dist/lib/prompt.test.d.ts +2 -0
  35. package/dist/lib/prompt.test.d.ts.map +1 -0
  36. package/dist/lib/transport.d.ts +59 -0
  37. package/dist/lib/transport.d.ts.map +1 -0
  38. package/dist/locales/en.d.ts +2 -0
  39. package/dist/locales/en.d.ts.map +1 -0
  40. package/dist/locales/index.d.ts +2 -0
  41. package/dist/locales/index.d.ts.map +1 -0
  42. package/dist/locales/pt-BR.d.ts +2 -0
  43. package/dist/locales/pt-BR.d.ts.map +1 -0
  44. package/dist/migrations/index.d.ts +6 -0
  45. package/dist/migrations/index.d.ts.map +1 -0
  46. package/dist/runtime/capture.d.ts +84 -0
  47. package/dist/runtime/capture.d.ts.map +1 -0
  48. package/dist/runtime/idb.d.ts +121 -0
  49. package/dist/runtime/idb.d.ts.map +1 -0
  50. package/dist/runtime/index.d.ts +48 -0
  51. package/dist/runtime/index.d.ts.map +1 -0
  52. package/dist/runtime/pump.d.ts +11 -0
  53. package/dist/runtime/pump.d.ts.map +1 -0
  54. package/dist/store.d.ts +25 -0
  55. package/dist/store.d.ts.map +1 -0
  56. package/dist/types.d.ts +266 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/functions/scribe-generate/index.ts +255 -0
  59. package/functions/scribe-transcribe/index.ts +455 -0
  60. package/package.json +58 -0
@@ -0,0 +1,266 @@
1
+ /**
2
+ * SERVER-side session state. Advances only on durable facts: a segment that
3
+ * reached Storage, a transcript that came back. Client state, which can die at
4
+ * any moment, lives separately in `CaptureState`.
5
+ */
6
+ export type SessionStatus = 'recording' | 'paused' | 'interrupted' | 'uploading' | 'transcribing' | 'ready' | 'generating' | 'completed' | 'abandoned' | 'failed';
7
+ /** States where the session is still someone's — used by the partial index and recovery. */
8
+ export declare const OPEN_SESSION_STATUSES: SessionStatus[];
9
+ /**
10
+ * CLIENT-side capture state. `interrupted` is the honest state between "the mic
11
+ * is gone" and "the user decided what to do" — collapsing it into `paused` would
12
+ * lose the distinction between intent and accident, which the transcript needs
13
+ * in order to record the hole.
14
+ */
15
+ export type CaptureState = 'idle' | 'consent' | 'requesting' | 'recording' | 'paused' | 'interrupted' | 'stopping' | 'error';
16
+ /** What the encounter is attached to. `standalone` records first, attaches later. */
17
+ export type SessionContextKind = 'person' | 'appointment' | 'standalone';
18
+ /**
19
+ * Legal basis recorded at capture time. Consent is what separates the recording
20
+ * from a problem, and "when" and "how" is what gets proven later.
21
+ * `implied_contract` covers recording as part of a contracted service.
22
+ */
23
+ export type ConsentMode = 'verbal' | 'written' | 'implied_contract';
24
+ export type UploadState = 'pending' | 'uploading' | 'uploaded' | 'failed' | 'missing';
25
+ export type SttState = 'pending' | 'running' | 'done' | 'failed' | 'skipped';
26
+ export type GenerationStatus = 'pending' | 'running' | 'ready' | 'failed' | 'committed' | 'discarded';
27
+ export interface ScribeSession {
28
+ id: string;
29
+ tenantId: string;
30
+ subjectId?: string;
31
+ subjectName?: string;
32
+ contextKind: SessionContextKind;
33
+ appointmentId?: string;
34
+ ownerUserId: string;
35
+ status: SessionStatus;
36
+ locale: string;
37
+ mimeType?: string;
38
+ startedAt: string;
39
+ endedAt?: string;
40
+ /** Wall clock reported by the client. Moves the pill; not a record. */
41
+ wallDurationMs: number;
42
+ /** SUM(segments.duration_ms) — the only duration that counts for records, limits and billing. */
43
+ audioDurationMs: number;
44
+ segmentCount: number;
45
+ uploadedSegmentCount: number;
46
+ transcribedSegmentCount: number;
47
+ transcriptChars: number;
48
+ consentAt?: string;
49
+ consentMode?: ConsentMode;
50
+ retentionUntil?: string;
51
+ audioDeletedAt?: string;
52
+ error?: string;
53
+ metadata: Record<string, unknown>;
54
+ }
55
+ /** A word, offset RELATIVE to the start of its segment. */
56
+ export interface TranscriptWord {
57
+ /** word */
58
+ w: string;
59
+ /** start ms, relative to the segment */
60
+ s: number;
61
+ /** end ms, relative to the segment */
62
+ e: number;
63
+ /** confidence 0..1 */
64
+ c?: number;
65
+ /** speaker index, when diarized */
66
+ sp?: number;
67
+ }
68
+ export interface ScribeSegment {
69
+ sessionId: string;
70
+ segIndex: number;
71
+ tenantId: string;
72
+ storagePath?: string;
73
+ bytes?: number;
74
+ /** Offset of this segment from the start of the session. */
75
+ startOffsetMs: number;
76
+ /** From the STT response, not a JS clock. */
77
+ durationMs?: number;
78
+ /** Assembled from pieces of a segment interrupted mid-way. */
79
+ partial: boolean;
80
+ /**
81
+ * SYNTHETIC row marking a hole (sleep, mic lost, permission revoked). Carries
82
+ * no audio. It exists so the transcript admits the hole instead of silently
83
+ * splicing two moments together — a medico-legal requirement, not polish.
84
+ */
85
+ gap: boolean;
86
+ uploadState: UploadState;
87
+ uploadedAt?: string;
88
+ sttState: SttState;
89
+ sttProvider?: string;
90
+ sttAttempts: number;
91
+ sttError?: string;
92
+ text?: string;
93
+ words?: TranscriptWord[];
94
+ confidence?: number;
95
+ }
96
+ export interface ScribeGeneration {
97
+ id: string;
98
+ tenantId: string;
99
+ sessionId: string;
100
+ /** Tenant template row. NULL when the generation came from a code preset. */
101
+ templateId?: string;
102
+ /** Preset key (`beauty.anamnesis.v1`) when there is no row. */
103
+ templateKey?: string;
104
+ tabOrder: number;
105
+ title?: string;
106
+ status: GenerationStatus;
107
+ /** Model output. Written once, NEVER mutated. */
108
+ markdown?: string;
109
+ /** The human's working copy. The difference between the two is what proves review. */
110
+ markdownEdited?: string;
111
+ /** Stage-1 extraction (two-stage). Reserved; the column exists from v0 on purpose. */
112
+ facts?: Record<string, unknown>;
113
+ model?: string;
114
+ promptVersion?: string;
115
+ inputTokens?: number;
116
+ outputTokens?: number;
117
+ error?: string;
118
+ documentId?: string;
119
+ createdBy?: string;
120
+ createdAt: string;
121
+ }
122
+ export type SectionShape = 'prose' | 'bullets' | 'keyvalue';
123
+ export interface NarrativeSectionDef {
124
+ /** Stable machine key. NEVER translated — it outlives heading renames. */
125
+ id: string;
126
+ /** Becomes `## <heading>` in the markdown. Translatable. */
127
+ heading: string;
128
+ /**
129
+ * The one per-section prompt the model sees. Write it as an instruction to a
130
+ * junior: what belongs here, what does not, in which voice.
131
+ */
132
+ guidance: string;
133
+ /**
134
+ * Controls the SHAPE of the markdown. `keyvalue` exists so structured data
135
+ * comes out as `- **Label:** value` rather than a pipe table — the SDK
136
+ * renderer has no table, and a broken A4 prints garbage.
137
+ */
138
+ shape?: SectionShape;
139
+ required?: boolean;
140
+ /** Drops the whole section, heading included, when the transcript said nothing. */
141
+ omitWhenEmpty?: boolean;
142
+ maxWords?: number;
143
+ /** Fact keys this section consumes in two-stage mode. Reserved. */
144
+ facts?: string[];
145
+ }
146
+ /** Where each header field is pulled from. Resolved from DATA, never asked of the model. */
147
+ export type NarrativeHeaderSource = 'subject.name' | 'subject.documentNumber' | 'subject.birthDate' | 'session.startedAt' | 'session.durationMinutes' | 'operator.name' | (string & {});
148
+ export interface NarrativeSchema {
149
+ kind: 'narrative';
150
+ sections: NarrativeSectionDef[];
151
+ /** Tone, person, tense, jargon level. Injected ahead of the sections. */
152
+ style?: string;
153
+ /** OUTPUT language, independent of the transcript's. */
154
+ outputLocale?: string;
155
+ /** Block above the first section, resolved from data. */
156
+ header?: {
157
+ fields: Array<{
158
+ label: string;
159
+ source: NarrativeHeaderSource;
160
+ }>;
161
+ };
162
+ /** Footer, e.g. 'AI-generated from a recording, reviewed by the professional.' */
163
+ disclaimer?: string;
164
+ /**
165
+ * Things the model must report as "not stated" instead of inferring, e.g.
166
+ * ['diagnosis', 'dosage', 'ICD']. The line between summarising and inventing.
167
+ */
168
+ neverInfer?: string[];
169
+ }
170
+ export declare function isNarrativeSchema(schema: unknown): schema is NarrativeSchema;
171
+ /**
172
+ * A system template, defined in CODE rather than in a SQL seed. Fixing a prompt
173
+ * becomes a release instead of a migration editing a row the user may have
174
+ * changed, and the default does not eat the tenant's template quota. Customising
175
+ * forks it, and the fork is the row.
176
+ */
177
+ export interface NarrativeTemplatePreset {
178
+ /** Versioned in the key itself: `beauty.anamnesis.v1`. */
179
+ key: string;
180
+ name: string;
181
+ description?: string;
182
+ /** plugin-forms category: anamnesis | evolution | report | contract | general. */
183
+ category: string;
184
+ specialty?: string;
185
+ schema: NarrativeSchema;
186
+ }
187
+ export interface ScribeLabels {
188
+ /** "Appointment" / "Visit" / "Meeting" */
189
+ sessionSingular: string;
190
+ sessionPlural: string;
191
+ /** "Start appointment" */
192
+ start: string;
193
+ pause: string;
194
+ resume: string;
195
+ finish: string;
196
+ discard: string;
197
+ /** "Generate document" / "Generate draft" */
198
+ generate: string;
199
+ /** "Client" / "Patient" */
200
+ subject: string;
201
+ transcript: string;
202
+ settingsTitle: string;
203
+ }
204
+ /**
205
+ * Where the model credential lives. See `lib/transport.ts` for why there are
206
+ * exactly these two values and no third.
207
+ */
208
+ export type ScribeTransportKind = 'fayz' | 'edge';
209
+ export interface ScribeSttConfig {
210
+ /**
211
+ * Defaults to `'fayz'`: the platform API, the same broker the app assistant
212
+ * already uses — no secret and no deploy on the app side. `'edge'` keeps the
213
+ * own-edge-function path, for anyone running off-platform or needing a
214
+ * provider the broker does not offer.
215
+ */
216
+ transport?: ScribeTransportKind;
217
+ /**
218
+ * Provider behind a one-method seam — swapping must not be a refactor. The
219
+ * platform broker speaks OpenAI; Deepgram requires `transport: 'edge'`.
220
+ */
221
+ provider: 'deepgram' | 'openai';
222
+ /**
223
+ * Explicit, with no hidden default, because the choice shows: `whisper-1`
224
+ * returns duration and per-word offsets, while the `gpt-4o-*-transcribe`
225
+ * models are usually better at pt-BR but return text only — and then the
226
+ * authoritative duration has to come from somewhere else.
227
+ */
228
+ model?: string;
229
+ locale: string;
230
+ /** Only Deepgram diarizes today; ignored on the `fayz` transport. */
231
+ diarize?: boolean;
232
+ /**
233
+ * Transcription edge function name. REQUIRED when `transport: 'edge'`: with no
234
+ * transport configured the plugin refuses to boot, because the only remaining
235
+ * fallback would be the browser's native recognizer, which ships the session
236
+ * audio to an undeclared third party. See the invariant in `createScribePlugin`.
237
+ */
238
+ endpoint?: string;
239
+ }
240
+ export interface ScribeRetentionConfig {
241
+ /** Days until the audio may be deleted. Transcript and document survive. */
242
+ audioDays: number;
243
+ /** When true, no byte is recorded before the consent gate. */
244
+ requireConsent: boolean;
245
+ defaultConsentMode: ConsentMode;
246
+ }
247
+ export interface ScribeLimitsConfig {
248
+ minutesMonth?: string;
249
+ documentsMonth?: string;
250
+ }
251
+ export interface ScribePluginOptions {
252
+ /** `kind` of the `people` row allowed as subject ('customer', 'client', …). */
253
+ subjectKind?: string;
254
+ contextEntities?: string[];
255
+ labels?: Partial<ScribeLabels>;
256
+ /** The vertical's default templates — the ONLY new code a vertical writes. */
257
+ templatePresets?: NarrativeTemplatePreset[];
258
+ stt: ScribeSttConfig;
259
+ retention?: Partial<ScribeRetentionConfig>;
260
+ limits?: ScribeLimitsConfig;
261
+ /** Generation edge function name. */
262
+ generateEndpoint?: string;
263
+ scope?: 'universal' | 'vertical';
264
+ verticalId?: string;
265
+ }
266
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,255 @@
1
+ // scribe-generate — transcrição + template narrativo → documento em Markdown.
2
+ //
3
+ // Env: OPENAI_API_KEY (ou ANTHROPIC_API_KEY), SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY.
4
+ //
5
+ // ---------------------------------------------------------------------------
6
+ // Por que isto NÃO é uma tool do assistente de chat
7
+ // ---------------------------------------------------------------------------
8
+ // O turn loop do chat tem teto de 4 rodadas, vive num hook React que morre na
9
+ // navegação, executa no plano do cliente, e persiste tudo que passa por ele no
10
+ // histórico de conversa do broker. Gerar um documento clínico precisa do
11
+ // oposto em todos os quatro pontos: sem teto de rodada, sem depender de aba,
12
+ // no servidor, e sem despejar 9k tokens de transcrição de saúde num store de
13
+ // conversa que ninguém revisou.
14
+ //
15
+ // Além disso a geração precisa ser AUDITÁVEL (modelo, versão de prompt, tokens)
16
+ // e REPETÍVEL. Um turno de chat não é nem um nem outro.
17
+ //
18
+ // O assistente entra depois, na tela do rascunho, para reescrever UMA seção —
19
+ // isso sim cabe folgado em 4 rodadas.
20
+ //
21
+ // ---------------------------------------------------------------------------
22
+ // callModel: um ponto, um dia trocável
23
+ // ---------------------------------------------------------------------------
24
+ // Toda chamada de modelo passa por `callModel`. Quando o broker Fayz expuser um
25
+ // endpoint headless (`POST /agents/complete`, sem conversa persistida), a troca
26
+ // é o corpo desta função e nenhum app volta a segurar chave de modelo.
27
+ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.45.0'
28
+ import {
29
+ PROMPT_VERSION,
30
+ buildSystemPrompt,
31
+ buildUserPrompt,
32
+ deriveTitle,
33
+ renderTranscript,
34
+ type TranscriptLine,
35
+ } from '../../src/lib/prompt.ts'
36
+ import type { NarrativeSchema } from '../../src/types.ts'
37
+
38
+ const corsHeaders = {
39
+ 'Access-Control-Allow-Origin': '*',
40
+ 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
41
+ }
42
+
43
+ const DEFAULT_MODEL = 'gpt-4o'
44
+
45
+ interface ModelResult {
46
+ text: string
47
+ model: string
48
+ inputTokens?: number
49
+ outputTokens?: number
50
+ }
51
+
52
+ async function callModel(args: { system: string; user: string; model?: string }): Promise<ModelResult> {
53
+ const key = Deno.env.get('OPENAI_API_KEY')
54
+ if (!key) throw new Error('OPENAI_API_KEY não configurada')
55
+ const model = args.model ?? Deno.env.get('SCRIBE_MODEL') ?? DEFAULT_MODEL
56
+
57
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
58
+ method: 'POST',
59
+ headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({
61
+ model,
62
+ messages: [
63
+ { role: 'system', content: args.system },
64
+ { role: 'user', content: args.user },
65
+ ],
66
+ // Baixa mas não zero: documento clínico quer consistência, e temperatura
67
+ // zero deixa o modelo repetitivo em texto longo.
68
+ temperature: 0.2,
69
+ max_tokens: 4000,
70
+ }),
71
+ })
72
+ if (!res.ok) throw new Error(`modelo ${res.status}: ${await res.text()}`)
73
+
74
+ const json = await res.json()
75
+ return {
76
+ text: json?.choices?.[0]?.message?.content ?? '',
77
+ model,
78
+ inputTokens: json?.usage?.prompt_tokens,
79
+ outputTokens: json?.usage?.completion_tokens,
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Monta a transcrição na ordem dos segmentos. Buraco entra como linha, não é
85
+ * pulado — a costura invisível é o modo de falha que este design recusa.
86
+ */
87
+ async function loadTranscript(supabase: any, sessionId: string): Promise<TranscriptLine[]> {
88
+ const { data } = await supabase
89
+ .from('plg_scribe_segments')
90
+ .select('seg_index, start_offset_ms, text, gap, words')
91
+ .eq('session_id', sessionId)
92
+ .order('seg_index', { ascending: true })
93
+ if (!data) return []
94
+
95
+ return data.map((s: any) => ({
96
+ startOffsetMs: s.start_offset_ms ?? 0,
97
+ text: s.text ?? '',
98
+ gap: !!s.gap,
99
+ speaker: Array.isArray(s.words) && s.words.length > 0 ? s.words[0]?.sp : undefined,
100
+ }))
101
+ }
102
+
103
+ /**
104
+ * Resolve o schema: linha do tenant (forkada) ou preset vindo do cliente. Preset
105
+ * de sistema de propósito NÃO tem linha — ele não deve consumir a cota de
106
+ * templates do plano, e corrigir seu prompt deve ser release, não migration.
107
+ */
108
+ async function resolveSchema(
109
+ supabase: any,
110
+ templateId: string | undefined,
111
+ inlineSchema: NarrativeSchema | undefined,
112
+ ): Promise<{ schema: NarrativeSchema; name: string }> {
113
+ if (templateId) {
114
+ const { data } = await supabase
115
+ .from('plg_forms_templates')
116
+ .select('name, schema')
117
+ .eq('id', templateId)
118
+ .single()
119
+ if (data?.schema?.kind === 'narrative') return { schema: data.schema, name: data.name }
120
+ throw new Error('template não é narrativo')
121
+ }
122
+ if (inlineSchema?.kind === 'narrative') return { schema: inlineSchema, name: 'Documento' }
123
+ throw new Error('nenhum template informado')
124
+ }
125
+
126
+ Deno.serve(async (req: Request) => {
127
+ if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
128
+
129
+ const supabase = createClient(
130
+ Deno.env.get('SUPABASE_URL') ?? '',
131
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
132
+ { auth: { persistSession: false } },
133
+ )
134
+
135
+ let generationId: string | null = null
136
+
137
+ try {
138
+ const body = await req.json()
139
+ const sessionId = body.sessionId as string
140
+ if (!sessionId) throw new Error('sessionId é obrigatório')
141
+
142
+ // Mesma checagem do scribe-transcribe: a service key contorna a RLS, então
143
+ // a autorização é explícita aqui.
144
+ const jwt = (req.headers.get('Authorization') ?? '').replace(/^Bearer\s+/i, '')
145
+ if (!jwt) return json({ error: 'não autenticado' }, 401)
146
+ const { data: caller } = await supabase.auth.getUser(jwt)
147
+ if (!caller?.user) return json({ error: 'não autenticado' }, 401)
148
+
149
+ const { data: session } = await supabase
150
+ .from('plg_scribe_sessions')
151
+ .select('id, tenant_id, subject_id, started_at, audio_duration_ms, locale')
152
+ .eq('id', sessionId)
153
+ .single()
154
+ if (!session) return json({ error: 'sessão não encontrada' }, 404)
155
+
156
+ const { data: membership } = await supabase
157
+ .schema('saas_core')
158
+ .from('tenant_members')
159
+ .select('user_id')
160
+ .eq('tenant_id', session.tenant_id)
161
+ .eq('user_id', caller.user.id)
162
+ .maybeSingle()
163
+ if (!membership) return json({ error: 'acesso negado' }, 403)
164
+
165
+ const { schema, name } = await resolveSchema(supabase, body.templateId, body.schema)
166
+
167
+ const lines = await loadTranscript(supabase, sessionId)
168
+ const hasAudio = lines.some((l) => !l.gap && l.text.trim())
169
+ if (!hasAudio) return json({ error: 'não há transcrição para gerar o documento' }, 422)
170
+
171
+ // A linha nasce em `running` ANTES da chamada ao modelo: uma geração que
172
+ // trava sem deixar rastro é indistinguível de uma que nunca foi pedida.
173
+ const { data: created, error: insErr } = await supabase
174
+ .from('plg_scribe_generations')
175
+ .insert({
176
+ tenant_id: session.tenant_id,
177
+ session_id: sessionId,
178
+ template_id: body.templateId ?? null,
179
+ template_key: body.templateKey ?? null,
180
+ tab_order: body.tabOrder ?? 0,
181
+ status: 'running',
182
+ prompt_version: PROMPT_VERSION,
183
+ created_by: caller.user.id,
184
+ })
185
+ .select('id')
186
+ .single()
187
+ if (insErr || !created) throw new Error(`falha ao criar a geração: ${insErr?.message}`)
188
+ generationId = created.id
189
+
190
+ let subjectName: string | undefined
191
+ if (session.subject_id) {
192
+ const { data: person } = await supabase
193
+ .from('people')
194
+ .select('name')
195
+ .eq('id', session.subject_id)
196
+ .maybeSingle()
197
+ subjectName = person?.name
198
+ }
199
+
200
+ const result = await callModel({
201
+ system: buildSystemPrompt(schema),
202
+ user: buildUserPrompt({
203
+ schema,
204
+ transcript: renderTranscript(lines),
205
+ context: {
206
+ Titular: subjectName,
207
+ Data: new Date(session.started_at).toLocaleDateString(session.locale ?? 'pt-BR'),
208
+ 'Duração (min)': String(Math.round((session.audio_duration_ms ?? 0) / 60000)),
209
+ },
210
+ }),
211
+ model: body.model,
212
+ })
213
+
214
+ const markdown = result.text.trim()
215
+
216
+ await supabase
217
+ .from('plg_scribe_generations')
218
+ .update({
219
+ status: 'ready',
220
+ // `markdown` é escrito uma vez e nunca mais. A edição do humano vai
221
+ // para `markdown_edited`, e a diferença entre os dois é a prova de que
222
+ // alguém revisou.
223
+ markdown,
224
+ title: deriveTitle(markdown, name),
225
+ model: result.model,
226
+ input_tokens: result.inputTokens,
227
+ output_tokens: result.outputTokens,
228
+ updated_at: new Date().toISOString(),
229
+ })
230
+ .eq('id', generationId)
231
+
232
+ await supabase
233
+ .from('plg_scribe_sessions')
234
+ .update({ status: 'generating', updated_at: new Date().toISOString() })
235
+ .eq('id', sessionId)
236
+
237
+ return json({ ok: true, generationId, markdown, model: result.model })
238
+ } catch (err) {
239
+ const message = String((err as Error)?.message ?? err)
240
+ if (generationId) {
241
+ await supabase
242
+ .from('plg_scribe_generations')
243
+ .update({ status: 'failed', error: message.slice(0, 500), updated_at: new Date().toISOString() })
244
+ .eq('id', generationId)
245
+ }
246
+ return json({ error: message }, 500)
247
+ }
248
+ })
249
+
250
+ function json(payload: unknown, status = 200): Response {
251
+ return new Response(JSON.stringify(payload), {
252
+ status,
253
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
254
+ })
255
+ }