@opengeni/db 0.27.9 → 0.28.1
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/dist/{chunk-2JFRTKTG.js → chunk-DBS5HPEW.js} +11 -1
- package/dist/chunk-DBS5HPEW.js.map +1 -0
- package/dist/{chunk-NX2JNJJP.js → chunk-HT5T62CC.js} +2538 -2283
- package/dist/chunk-HT5T62CC.js.map +1 -0
- package/dist/index.d.ts +69 -36
- package/dist/index.js +4146 -2924
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +129 -96
- package/dist/schema.js +19 -1
- package/dist/transcription-recordings-schema.d.ts +1226 -0
- package/dist/transcription-recordings.d.ts +228 -0
- package/drizzle/0170_resumable_transcription_recordings.sql +440 -0
- package/drizzle/0172_retire_model_visible_github_token.sql +86 -0
- package/drizzle/0173_codex_auth_boundaries.sql +107 -0
- package/drizzle/0174_session_wake_live_interruption.sql +80 -0
- package/drizzle/0175_resumable_transcription_provider_deadline.sql +24 -0
- package/package.json +4 -4
- package/src/index.ts +585 -245
- package/src/runtime-posture.ts +10 -0
- package/src/schema.ts +63 -51
- package/src/transcription-recordings-schema.ts +250 -0
- package/src/transcription-recordings.ts +1508 -0
- package/dist/chunk-2JFRTKTG.js.map +0 -1
- package/dist/chunk-NX2JNJJP.js.map +0 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { TranscriptionRecording, TranscriptionRecordingErrorCode, TranscriptionRecordingResponse } from "@opengeni/contracts";
|
|
2
|
+
import type { Database } from "./database";
|
|
3
|
+
import * as schema from "./schema";
|
|
4
|
+
type ChunkRow = typeof schema.transcriptionRecordingChunks.$inferSelect;
|
|
5
|
+
type SegmentRow = typeof schema.transcriptionRecordingSegments.$inferSelect;
|
|
6
|
+
export declare class TranscriptionRecordingNotFoundError extends Error {
|
|
7
|
+
readonly name = "TranscriptionRecordingNotFoundError";
|
|
8
|
+
}
|
|
9
|
+
export declare class TranscriptionRecordingConflictError extends Error {
|
|
10
|
+
readonly name = "TranscriptionRecordingConflictError";
|
|
11
|
+
}
|
|
12
|
+
export declare class TranscriptionRecordingStateError extends Error {
|
|
13
|
+
readonly name = "TranscriptionRecordingStateError";
|
|
14
|
+
}
|
|
15
|
+
export type TranscriptionRecordingChunkReservation = {
|
|
16
|
+
recording: TranscriptionRecordingResponse;
|
|
17
|
+
chunk: ChunkRow;
|
|
18
|
+
deduplicated: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type TranscriptionRecordingAssemblyClaim = {
|
|
21
|
+
recording: TranscriptionRecordingResponse;
|
|
22
|
+
claimed: boolean;
|
|
23
|
+
generation: number;
|
|
24
|
+
owner: string | null;
|
|
25
|
+
staleObjectKeys: string[];
|
|
26
|
+
};
|
|
27
|
+
export type TranscriptionRecordingSegmentClaim = {
|
|
28
|
+
recording: TranscriptionRecordingResponse;
|
|
29
|
+
claimed: boolean;
|
|
30
|
+
attemptId: string | null;
|
|
31
|
+
segment: SegmentRow | null;
|
|
32
|
+
};
|
|
33
|
+
export type TranscriptionRecordingObjectCleanupClaim = {
|
|
34
|
+
accountId: string;
|
|
35
|
+
workspaceId: string;
|
|
36
|
+
subjectId: string;
|
|
37
|
+
recordingId: string;
|
|
38
|
+
objectKey: string;
|
|
39
|
+
cleanupClaimId: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A stale attempt is reclaimable only after both durable fences have expired:
|
|
43
|
+
* the processing lease and the server-owned provider deadline. Missing
|
|
44
|
+
* deadlines fail closed so an old pre-deadline row cannot create an overlap.
|
|
45
|
+
*/
|
|
46
|
+
export declare function canReclaimTranscriptionRecordingAttempt(input: {
|
|
47
|
+
attemptStartedAt: Date | null;
|
|
48
|
+
attemptDeadlineAt: Date | null;
|
|
49
|
+
staleBefore: Date;
|
|
50
|
+
now: Date;
|
|
51
|
+
}): boolean;
|
|
52
|
+
export declare function transcriptionRecordingChunkObjectKey(input: {
|
|
53
|
+
accountId: string;
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
recordingId: string;
|
|
56
|
+
chunkNumber: number;
|
|
57
|
+
sha256: string;
|
|
58
|
+
}): string;
|
|
59
|
+
export declare function transcriptionRecordingSegmentObjectKey(input: {
|
|
60
|
+
accountId: string;
|
|
61
|
+
workspaceId: string;
|
|
62
|
+
recordingId: string;
|
|
63
|
+
generation: number;
|
|
64
|
+
segmentNumber: number;
|
|
65
|
+
sha256: string;
|
|
66
|
+
}): string;
|
|
67
|
+
export declare function createTranscriptionRecording(db: Database, input: {
|
|
68
|
+
accountId: string;
|
|
69
|
+
workspaceId: string;
|
|
70
|
+
subjectId: string;
|
|
71
|
+
recordingId: string;
|
|
72
|
+
mimeType: string;
|
|
73
|
+
expiresAt: Date;
|
|
74
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
75
|
+
export declare function getTranscriptionRecording(db: Database, input: {
|
|
76
|
+
workspaceId: string;
|
|
77
|
+
subjectId: string;
|
|
78
|
+
recordingId: string;
|
|
79
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
80
|
+
export declare function listTranscriptionRecordings(db: Database, input: {
|
|
81
|
+
workspaceId: string;
|
|
82
|
+
subjectId: string;
|
|
83
|
+
limit?: number;
|
|
84
|
+
}): Promise<TranscriptionRecording[]>;
|
|
85
|
+
export declare function reserveTranscriptionRecordingChunk(db: Database, input: {
|
|
86
|
+
accountId: string;
|
|
87
|
+
workspaceId: string;
|
|
88
|
+
subjectId: string;
|
|
89
|
+
recordingId: string;
|
|
90
|
+
chunkNumber: number;
|
|
91
|
+
byteLength: number;
|
|
92
|
+
sha256: string;
|
|
93
|
+
startMilliseconds: number;
|
|
94
|
+
durationMilliseconds: number;
|
|
95
|
+
maxTotalBytes: number;
|
|
96
|
+
maxDurationMilliseconds: number;
|
|
97
|
+
}): Promise<TranscriptionRecordingChunkReservation>;
|
|
98
|
+
export declare function completeTranscriptionRecordingChunk(db: Database, input: {
|
|
99
|
+
workspaceId: string;
|
|
100
|
+
subjectId: string;
|
|
101
|
+
recordingId: string;
|
|
102
|
+
chunkNumber: number;
|
|
103
|
+
}): Promise<TranscriptionRecordingChunkReservation>;
|
|
104
|
+
export declare function claimTranscriptionRecordingAssembly(db: Database, input: {
|
|
105
|
+
workspaceId: string;
|
|
106
|
+
subjectId: string;
|
|
107
|
+
recordingId: string;
|
|
108
|
+
owner: string;
|
|
109
|
+
chunkCount: number;
|
|
110
|
+
totalBytes: number;
|
|
111
|
+
totalDurationMilliseconds: number;
|
|
112
|
+
staleBefore: Date;
|
|
113
|
+
}): Promise<TranscriptionRecordingAssemblyClaim>;
|
|
114
|
+
export declare function listTranscriptionRecordingChunks(db: Database, input: {
|
|
115
|
+
workspaceId: string;
|
|
116
|
+
subjectId: string;
|
|
117
|
+
recordingId: string;
|
|
118
|
+
}): Promise<ChunkRow[]>;
|
|
119
|
+
export declare function reserveTranscriptionRecordingSegment(db: Database, input: {
|
|
120
|
+
accountId: string;
|
|
121
|
+
workspaceId: string;
|
|
122
|
+
subjectId: string;
|
|
123
|
+
recordingId: string;
|
|
124
|
+
owner: string;
|
|
125
|
+
generation: number;
|
|
126
|
+
segmentNumber: number;
|
|
127
|
+
byteLength: number;
|
|
128
|
+
sha256: string;
|
|
129
|
+
startMilliseconds: number;
|
|
130
|
+
durationMilliseconds: number;
|
|
131
|
+
}): Promise<SegmentRow>;
|
|
132
|
+
export declare function completeTranscriptionRecordingSegmentPreparation(db: Database, input: {
|
|
133
|
+
workspaceId: string;
|
|
134
|
+
subjectId: string;
|
|
135
|
+
recordingId: string;
|
|
136
|
+
owner: string;
|
|
137
|
+
generation: number;
|
|
138
|
+
segmentNumber: number;
|
|
139
|
+
}): Promise<void>;
|
|
140
|
+
export declare function completeTranscriptionRecordingAssembly(db: Database, input: {
|
|
141
|
+
workspaceId: string;
|
|
142
|
+
subjectId: string;
|
|
143
|
+
recordingId: string;
|
|
144
|
+
owner: string;
|
|
145
|
+
generation: number;
|
|
146
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
147
|
+
export declare function failTranscriptionRecordingAssembly(db: Database, input: {
|
|
148
|
+
workspaceId: string;
|
|
149
|
+
subjectId: string;
|
|
150
|
+
recordingId: string;
|
|
151
|
+
owner: string;
|
|
152
|
+
generation: number;
|
|
153
|
+
errorCode: TranscriptionRecordingErrorCode;
|
|
154
|
+
retryable: boolean;
|
|
155
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
156
|
+
export declare function claimNextTranscriptionRecordingSegment(db: Database, input: {
|
|
157
|
+
workspaceId: string;
|
|
158
|
+
subjectId: string;
|
|
159
|
+
recordingId: string;
|
|
160
|
+
attemptId: string;
|
|
161
|
+
providerId: string;
|
|
162
|
+
staleBefore: Date;
|
|
163
|
+
providerDeadlineAt: Date;
|
|
164
|
+
}): Promise<TranscriptionRecordingSegmentClaim>;
|
|
165
|
+
export declare function startTranscriptionRecordingSegmentProviderCall(db: Database, input: {
|
|
166
|
+
workspaceId: string;
|
|
167
|
+
subjectId: string;
|
|
168
|
+
recordingId: string;
|
|
169
|
+
segmentNumber: number;
|
|
170
|
+
attemptId: string;
|
|
171
|
+
providerStartedAt: Date;
|
|
172
|
+
providerDeadlineAt: Date;
|
|
173
|
+
}): Promise<void>;
|
|
174
|
+
export declare function assembleTranscriptionSegments(segments: readonly Pick<SegmentRow, "segmentNumber" | "transcriptText" | "languages">[]): {
|
|
175
|
+
text: string;
|
|
176
|
+
languages: string[];
|
|
177
|
+
};
|
|
178
|
+
export declare function completeTranscriptionRecordingSegment(db: Database, input: {
|
|
179
|
+
workspaceId: string;
|
|
180
|
+
subjectId: string;
|
|
181
|
+
recordingId: string;
|
|
182
|
+
segmentNumber: number;
|
|
183
|
+
attemptId: string;
|
|
184
|
+
text: string;
|
|
185
|
+
languages: string[];
|
|
186
|
+
providerId: string;
|
|
187
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
188
|
+
export declare function failTranscriptionRecordingSegment(db: Database, input: {
|
|
189
|
+
workspaceId: string;
|
|
190
|
+
subjectId: string;
|
|
191
|
+
recordingId: string;
|
|
192
|
+
segmentNumber: number;
|
|
193
|
+
attemptId: string;
|
|
194
|
+
errorCode: TranscriptionRecordingErrorCode;
|
|
195
|
+
retryable: boolean;
|
|
196
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
197
|
+
export declare function transcriptionRecordingObjectKeys(db: Database, input: {
|
|
198
|
+
workspaceId: string;
|
|
199
|
+
subjectId: string;
|
|
200
|
+
recordingId: string;
|
|
201
|
+
}): Promise<string[]>;
|
|
202
|
+
export declare function markTranscriptionRecordingObjectCleaned(db: Database, input: {
|
|
203
|
+
workspaceId: string;
|
|
204
|
+
subjectId: string;
|
|
205
|
+
recordingId: string;
|
|
206
|
+
objectKey: string;
|
|
207
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
208
|
+
export declare function markTranscriptionRecordingObjectsCleaned(db: Database, input: {
|
|
209
|
+
workspaceId: string;
|
|
210
|
+
subjectId: string;
|
|
211
|
+
recordingId: string;
|
|
212
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
213
|
+
export declare function discardTranscriptionRecording(db: Database, input: {
|
|
214
|
+
workspaceId: string;
|
|
215
|
+
subjectId: string;
|
|
216
|
+
recordingId: string;
|
|
217
|
+
}): Promise<TranscriptionRecordingResponse>;
|
|
218
|
+
export declare function claimDueTranscriptionRecordingObjectCleanup(db: Database, input: {
|
|
219
|
+
graceMs: number;
|
|
220
|
+
claimTimeoutMs: number;
|
|
221
|
+
limit: number;
|
|
222
|
+
}): Promise<TranscriptionRecordingObjectCleanupClaim[]>;
|
|
223
|
+
export declare function completeDueTranscriptionRecordingObjectCleanup(db: Database, input: TranscriptionRecordingObjectCleanupClaim): Promise<boolean>;
|
|
224
|
+
export declare function purgeExpiredTranscriptionRecordings(db: Database, input: {
|
|
225
|
+
graceMs: number;
|
|
226
|
+
limit: number;
|
|
227
|
+
}): Promise<number>;
|
|
228
|
+
export {};
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
|
|
3
|
+
CREATE TABLE "transcription_recordings" (
|
|
4
|
+
"id" uuid PRIMARY KEY,
|
|
5
|
+
"account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
|
|
6
|
+
"workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
|
|
7
|
+
"subject_id" text NOT NULL,
|
|
8
|
+
"mime_type" text NOT NULL,
|
|
9
|
+
"state" text NOT NULL DEFAULT 'uploading',
|
|
10
|
+
"next_chunk_number" integer NOT NULL DEFAULT 0,
|
|
11
|
+
"chunk_count" integer NOT NULL DEFAULT 0,
|
|
12
|
+
"total_bytes" integer NOT NULL DEFAULT 0,
|
|
13
|
+
"total_duration_milliseconds" integer NOT NULL DEFAULT 0,
|
|
14
|
+
"segment_count" integer NOT NULL DEFAULT 0,
|
|
15
|
+
"completed_segment_count" integer NOT NULL DEFAULT 0,
|
|
16
|
+
"transcript_text" text,
|
|
17
|
+
"languages" jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
18
|
+
"error_code" text,
|
|
19
|
+
"retryable" boolean NOT NULL DEFAULT false,
|
|
20
|
+
"provider_id" text,
|
|
21
|
+
"processing_generation" integer NOT NULL DEFAULT 0,
|
|
22
|
+
"processing_owner" uuid,
|
|
23
|
+
"processing_started_at" timestamptz,
|
|
24
|
+
"objects_cleaned_at" timestamptz,
|
|
25
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
26
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
27
|
+
"expires_at" timestamptz NOT NULL,
|
|
28
|
+
CONSTRAINT "transcription_recordings_workspace_account_fk"
|
|
29
|
+
FOREIGN KEY ("workspace_id", "account_id")
|
|
30
|
+
REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
|
|
31
|
+
CONSTRAINT "transcription_recordings_exact_authority_uq"
|
|
32
|
+
UNIQUE ("account_id", "workspace_id", "subject_id", "id"),
|
|
33
|
+
CONSTRAINT "transcription_recordings_state_check"
|
|
34
|
+
CHECK ("state" IN (
|
|
35
|
+
'uploading', 'segmenting', 'ready', 'transcribing', 'complete', 'failed', 'discarded'
|
|
36
|
+
)),
|
|
37
|
+
CONSTRAINT "transcription_recordings_values_check"
|
|
38
|
+
CHECK (
|
|
39
|
+
"next_chunk_number" >= 0
|
|
40
|
+
AND "chunk_count" >= 0
|
|
41
|
+
AND "total_bytes" >= 0
|
|
42
|
+
AND "total_duration_milliseconds" >= 0
|
|
43
|
+
AND "segment_count" >= 0
|
|
44
|
+
AND "completed_segment_count" >= 0
|
|
45
|
+
AND "completed_segment_count" <= "segment_count"
|
|
46
|
+
AND octet_length("subject_id") BETWEEN 1 AND 1024
|
|
47
|
+
AND octet_length("mime_type") BETWEEN 1 AND 128
|
|
48
|
+
AND ("provider_id" IS NULL OR octet_length("provider_id") BETWEEN 1 AND 128)
|
|
49
|
+
AND ("transcript_text" IS NULL OR octet_length("transcript_text") <= 4000000)
|
|
50
|
+
AND jsonb_typeof("languages") = 'array'
|
|
51
|
+
),
|
|
52
|
+
CONSTRAINT "transcription_recordings_error_code_check"
|
|
53
|
+
CHECK ("error_code" IS NULL OR "error_code" IN (
|
|
54
|
+
'permission_denied', 'not_supported', 'network', 'provider', 'policy_blocked',
|
|
55
|
+
'timeout', 'cancelled', 'unavailable', 'too_large', 'invalid_audio', 'unknown'
|
|
56
|
+
)),
|
|
57
|
+
CONSTRAINT "transcription_recordings_processing_check"
|
|
58
|
+
CHECK (
|
|
59
|
+
("processing_owner" IS NULL AND "processing_started_at" IS NULL)
|
|
60
|
+
OR ("processing_owner" IS NOT NULL AND "processing_started_at" IS NOT NULL)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE INDEX "transcription_recordings_subject_created_idx"
|
|
65
|
+
ON "transcription_recordings" ("workspace_id", "subject_id", "created_at");
|
|
66
|
+
CREATE INDEX "transcription_recordings_expiry_idx"
|
|
67
|
+
ON "transcription_recordings" ("expires_at", "id");
|
|
68
|
+
|
|
69
|
+
CREATE TABLE "transcription_recording_objects" (
|
|
70
|
+
"account_id" uuid NOT NULL,
|
|
71
|
+
"workspace_id" uuid NOT NULL,
|
|
72
|
+
"subject_id" text NOT NULL,
|
|
73
|
+
"recording_id" uuid NOT NULL,
|
|
74
|
+
"object_key" text PRIMARY KEY,
|
|
75
|
+
"kind" text NOT NULL,
|
|
76
|
+
"cleanup_after" timestamptz NOT NULL,
|
|
77
|
+
"cleanup_claim_id" uuid,
|
|
78
|
+
"cleanup_claimed_at" timestamptz,
|
|
79
|
+
"cleaned_at" timestamptz,
|
|
80
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
81
|
+
CONSTRAINT "transcription_recording_objects_authority_fk"
|
|
82
|
+
FOREIGN KEY ("account_id", "workspace_id", "subject_id", "recording_id")
|
|
83
|
+
REFERENCES "transcription_recordings"("account_id", "workspace_id", "subject_id", "id")
|
|
84
|
+
ON DELETE CASCADE,
|
|
85
|
+
CONSTRAINT "transcription_recording_objects_kind_check"
|
|
86
|
+
CHECK ("kind" IN ('chunk', 'segment')),
|
|
87
|
+
CONSTRAINT "transcription_recording_objects_values_check"
|
|
88
|
+
CHECK (
|
|
89
|
+
octet_length("object_key") BETWEEN 1 AND 1024
|
|
90
|
+
AND (
|
|
91
|
+
("cleanup_claim_id" IS NULL AND "cleanup_claimed_at" IS NULL)
|
|
92
|
+
OR ("cleanup_claim_id" IS NOT NULL AND "cleanup_claimed_at" IS NOT NULL)
|
|
93
|
+
)
|
|
94
|
+
AND (
|
|
95
|
+
"cleaned_at" IS NULL
|
|
96
|
+
OR ("cleanup_claim_id" IS NULL AND "cleanup_claimed_at" IS NULL)
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX "transcription_recording_objects_due_cleanup_idx"
|
|
102
|
+
ON "transcription_recording_objects" ("cleanup_after", "object_key")
|
|
103
|
+
WHERE "cleaned_at" IS NULL;
|
|
104
|
+
CREATE INDEX "transcription_recording_objects_claim_recovery_idx"
|
|
105
|
+
ON "transcription_recording_objects" ("cleanup_claimed_at", "object_key")
|
|
106
|
+
WHERE "cleaned_at" IS NULL AND "cleanup_claim_id" IS NOT NULL;
|
|
107
|
+
|
|
108
|
+
CREATE TABLE "transcription_recording_chunks" (
|
|
109
|
+
"account_id" uuid NOT NULL,
|
|
110
|
+
"workspace_id" uuid NOT NULL,
|
|
111
|
+
"subject_id" text NOT NULL,
|
|
112
|
+
"recording_id" uuid NOT NULL,
|
|
113
|
+
"chunk_number" integer NOT NULL,
|
|
114
|
+
"state" text NOT NULL DEFAULT 'uploading',
|
|
115
|
+
"byte_length" integer NOT NULL,
|
|
116
|
+
"sha256" text NOT NULL,
|
|
117
|
+
"start_milliseconds" integer NOT NULL,
|
|
118
|
+
"duration_milliseconds" integer NOT NULL,
|
|
119
|
+
"object_key" text NOT NULL,
|
|
120
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
121
|
+
"completed_at" timestamptz,
|
|
122
|
+
CONSTRAINT "transcription_recording_chunks_pk" PRIMARY KEY ("recording_id", "chunk_number"),
|
|
123
|
+
CONSTRAINT "transcription_recording_chunks_authority_fk"
|
|
124
|
+
FOREIGN KEY ("account_id", "workspace_id", "subject_id", "recording_id")
|
|
125
|
+
REFERENCES "transcription_recordings"("account_id", "workspace_id", "subject_id", "id")
|
|
126
|
+
ON DELETE CASCADE,
|
|
127
|
+
CONSTRAINT "transcription_recording_chunks_object_fk"
|
|
128
|
+
FOREIGN KEY ("object_key")
|
|
129
|
+
REFERENCES "transcription_recording_objects"("object_key") ON DELETE RESTRICT,
|
|
130
|
+
CONSTRAINT "transcription_recording_chunks_state_check"
|
|
131
|
+
CHECK ("state" IN ('uploading', 'complete')),
|
|
132
|
+
CONSTRAINT "transcription_recording_chunks_values_check"
|
|
133
|
+
CHECK (
|
|
134
|
+
"chunk_number" >= 0
|
|
135
|
+
AND "byte_length" > 0
|
|
136
|
+
AND "start_milliseconds" >= 0
|
|
137
|
+
AND "duration_milliseconds" >= 0
|
|
138
|
+
AND "sha256" ~ '^[0-9a-f]{64}$'
|
|
139
|
+
AND octet_length("object_key") BETWEEN 1 AND 1024
|
|
140
|
+
AND (
|
|
141
|
+
("state" = 'uploading' AND "completed_at" IS NULL)
|
|
142
|
+
OR ("state" = 'complete' AND "completed_at" IS NOT NULL)
|
|
143
|
+
)
|
|
144
|
+
),
|
|
145
|
+
CONSTRAINT "transcription_recording_chunks_object_key_uq" UNIQUE ("object_key")
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE INDEX "transcription_recording_chunks_order_idx"
|
|
149
|
+
ON "transcription_recording_chunks" ("workspace_id", "recording_id", "chunk_number");
|
|
150
|
+
|
|
151
|
+
CREATE TABLE "transcription_recording_segments" (
|
|
152
|
+
"account_id" uuid NOT NULL,
|
|
153
|
+
"workspace_id" uuid NOT NULL,
|
|
154
|
+
"subject_id" text NOT NULL,
|
|
155
|
+
"recording_id" uuid NOT NULL,
|
|
156
|
+
"segment_number" integer NOT NULL,
|
|
157
|
+
"generation" integer NOT NULL,
|
|
158
|
+
"state" text NOT NULL DEFAULT 'preparing',
|
|
159
|
+
"byte_length" integer NOT NULL,
|
|
160
|
+
"sha256" text NOT NULL,
|
|
161
|
+
"start_milliseconds" integer NOT NULL,
|
|
162
|
+
"duration_milliseconds" integer NOT NULL,
|
|
163
|
+
"object_key" text NOT NULL,
|
|
164
|
+
"attempt_id" uuid,
|
|
165
|
+
"attempt_started_at" timestamptz,
|
|
166
|
+
"transcript_text" text,
|
|
167
|
+
"languages" jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
168
|
+
"provider_id" text,
|
|
169
|
+
"error_code" text,
|
|
170
|
+
"retryable" boolean NOT NULL DEFAULT false,
|
|
171
|
+
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
172
|
+
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
173
|
+
CONSTRAINT "transcription_recording_segments_pk"
|
|
174
|
+
PRIMARY KEY ("recording_id", "segment_number"),
|
|
175
|
+
CONSTRAINT "transcription_recording_segments_authority_fk"
|
|
176
|
+
FOREIGN KEY ("account_id", "workspace_id", "subject_id", "recording_id")
|
|
177
|
+
REFERENCES "transcription_recordings"("account_id", "workspace_id", "subject_id", "id")
|
|
178
|
+
ON DELETE CASCADE,
|
|
179
|
+
CONSTRAINT "transcription_recording_segments_object_fk"
|
|
180
|
+
FOREIGN KEY ("object_key")
|
|
181
|
+
REFERENCES "transcription_recording_objects"("object_key") ON DELETE RESTRICT,
|
|
182
|
+
CONSTRAINT "transcription_recording_segments_state_check"
|
|
183
|
+
CHECK ("state" IN ('preparing', 'pending', 'transcribing', 'complete', 'failed')),
|
|
184
|
+
CONSTRAINT "transcription_recording_segments_values_check"
|
|
185
|
+
CHECK (
|
|
186
|
+
"segment_number" >= 0
|
|
187
|
+
AND "generation" > 0
|
|
188
|
+
AND "byte_length" > 0
|
|
189
|
+
AND "start_milliseconds" >= 0
|
|
190
|
+
AND "duration_milliseconds" > 0
|
|
191
|
+
AND "sha256" ~ '^[0-9a-f]{64}$'
|
|
192
|
+
AND octet_length("object_key") BETWEEN 1 AND 1024
|
|
193
|
+
AND ("transcript_text" IS NULL OR octet_length("transcript_text") <= 1000000)
|
|
194
|
+
AND jsonb_typeof("languages") = 'array'
|
|
195
|
+
AND (
|
|
196
|
+
("attempt_id" IS NULL AND "attempt_started_at" IS NULL)
|
|
197
|
+
OR ("attempt_id" IS NOT NULL AND "attempt_started_at" IS NOT NULL)
|
|
198
|
+
)
|
|
199
|
+
),
|
|
200
|
+
CONSTRAINT "transcription_recording_segments_error_code_check"
|
|
201
|
+
CHECK ("error_code" IS NULL OR "error_code" IN (
|
|
202
|
+
'permission_denied', 'not_supported', 'network', 'provider', 'policy_blocked',
|
|
203
|
+
'timeout', 'cancelled', 'unavailable', 'too_large', 'invalid_audio', 'unknown'
|
|
204
|
+
)),
|
|
205
|
+
CONSTRAINT "transcription_recording_segments_object_key_uq" UNIQUE ("object_key")
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
CREATE INDEX "transcription_recording_segments_order_idx"
|
|
209
|
+
ON "transcription_recording_segments" ("workspace_id", "recording_id", "segment_number");
|
|
210
|
+
|
|
211
|
+
ALTER TABLE "transcription_recordings" ENABLE ROW LEVEL SECURITY;
|
|
212
|
+
ALTER TABLE "transcription_recordings" FORCE ROW LEVEL SECURITY;
|
|
213
|
+
ALTER TABLE "transcription_recording_objects" ENABLE ROW LEVEL SECURITY;
|
|
214
|
+
ALTER TABLE "transcription_recording_objects" FORCE ROW LEVEL SECURITY;
|
|
215
|
+
ALTER TABLE "transcription_recording_chunks" ENABLE ROW LEVEL SECURITY;
|
|
216
|
+
ALTER TABLE "transcription_recording_chunks" FORCE ROW LEVEL SECURITY;
|
|
217
|
+
ALTER TABLE "transcription_recording_segments" ENABLE ROW LEVEL SECURITY;
|
|
218
|
+
ALTER TABLE "transcription_recording_segments" FORCE ROW LEVEL SECURITY;
|
|
219
|
+
|
|
220
|
+
CREATE POLICY transcription_recordings_subject_isolation ON "transcription_recordings"
|
|
221
|
+
USING (
|
|
222
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
223
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
224
|
+
)
|
|
225
|
+
WITH CHECK (
|
|
226
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
227
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
CREATE POLICY transcription_recording_objects_subject_isolation
|
|
231
|
+
ON "transcription_recording_objects"
|
|
232
|
+
USING (
|
|
233
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
234
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
235
|
+
)
|
|
236
|
+
WITH CHECK (
|
|
237
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
238
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
CREATE POLICY transcription_recording_chunks_subject_isolation
|
|
242
|
+
ON "transcription_recording_chunks"
|
|
243
|
+
USING (
|
|
244
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
245
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
246
|
+
)
|
|
247
|
+
WITH CHECK (
|
|
248
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
249
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
-- The existing provider-neutral object reaper needs one bounded cross-workspace
|
|
253
|
+
-- claim seam. The function returns only exact authority ids plus opaque storage
|
|
254
|
+
-- keys; no transcript, audio, credential, or provider metadata crosses the RLS
|
|
255
|
+
-- boundary. Recording rows are locked before object rows, matching request-side
|
|
256
|
+
-- mutation order. Expired recordings are fenced to discarded in the same claim.
|
|
257
|
+
-- The migration may target public or a dedicated embedded schema, so bind
|
|
258
|
+
-- current_schema() into every relation at creation time and expose only
|
|
259
|
+
-- pg_catalog at execution time.
|
|
260
|
+
DO $privileged_functions$
|
|
261
|
+
DECLARE data_schema text := current_schema();
|
|
262
|
+
BEGIN
|
|
263
|
+
EXECUTE format($ddl$
|
|
264
|
+
CREATE OR REPLACE FUNCTION opengeni_private.claim_due_transcription_recording_object_cleanup(
|
|
265
|
+
p_grace_ms bigint,
|
|
266
|
+
p_claim_timeout_ms bigint,
|
|
267
|
+
p_limit integer
|
|
268
|
+
)
|
|
269
|
+
RETURNS TABLE (
|
|
270
|
+
account_id uuid,
|
|
271
|
+
workspace_id uuid,
|
|
272
|
+
subject_id text,
|
|
273
|
+
recording_id uuid,
|
|
274
|
+
object_key text,
|
|
275
|
+
cleanup_claim_id uuid
|
|
276
|
+
)
|
|
277
|
+
LANGUAGE sql
|
|
278
|
+
SECURITY DEFINER
|
|
279
|
+
STRICT
|
|
280
|
+
SET search_path = pg_catalog
|
|
281
|
+
AS $function$
|
|
282
|
+
WITH due_recordings AS MATERIALIZED (
|
|
283
|
+
SELECT R.id, R.account_id, R.workspace_id, R.subject_id, R.expires_at
|
|
284
|
+
FROM %1$I.transcription_recordings R
|
|
285
|
+
WHERE EXISTS (
|
|
286
|
+
SELECT 1
|
|
287
|
+
FROM %1$I.transcription_recording_objects O
|
|
288
|
+
WHERE O.recording_id = R.id
|
|
289
|
+
AND O.cleaned_at IS NULL
|
|
290
|
+
AND O.cleanup_after <=
|
|
291
|
+
clock_timestamp() - greatest(p_grace_ms, 0) * interval '1 millisecond'
|
|
292
|
+
AND (
|
|
293
|
+
O.cleanup_claim_id IS NULL
|
|
294
|
+
OR O.cleanup_claimed_at <=
|
|
295
|
+
clock_timestamp() - greatest(p_claim_timeout_ms, 0) * interval '1 millisecond'
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
ORDER BY R.expires_at, R.id
|
|
299
|
+
FOR UPDATE OF R SKIP LOCKED
|
|
300
|
+
LIMIT least(greatest(p_limit, 0), 1000)
|
|
301
|
+
), expired_recordings AS (
|
|
302
|
+
UPDATE %1$I.transcription_recordings R
|
|
303
|
+
SET state = 'discarded',
|
|
304
|
+
processing_owner = NULL,
|
|
305
|
+
processing_started_at = NULL,
|
|
306
|
+
error_code = NULL,
|
|
307
|
+
retryable = false,
|
|
308
|
+
updated_at = clock_timestamp()
|
|
309
|
+
FROM due_recordings D
|
|
310
|
+
WHERE R.id = D.id
|
|
311
|
+
AND D.expires_at <=
|
|
312
|
+
clock_timestamp() - greatest(p_grace_ms, 0) * interval '1 millisecond'
|
|
313
|
+
AND R.state <> 'discarded'
|
|
314
|
+
RETURNING R.id
|
|
315
|
+
), candidates AS MATERIALIZED (
|
|
316
|
+
SELECT O.object_key
|
|
317
|
+
FROM %1$I.transcription_recording_objects O
|
|
318
|
+
JOIN due_recordings R ON R.id = O.recording_id
|
|
319
|
+
WHERE O.cleaned_at IS NULL
|
|
320
|
+
AND O.cleanup_after <=
|
|
321
|
+
clock_timestamp() - greatest(p_grace_ms, 0) * interval '1 millisecond'
|
|
322
|
+
AND (
|
|
323
|
+
O.cleanup_claim_id IS NULL
|
|
324
|
+
OR O.cleanup_claimed_at <=
|
|
325
|
+
clock_timestamp() - greatest(p_claim_timeout_ms, 0) * interval '1 millisecond'
|
|
326
|
+
)
|
|
327
|
+
ORDER BY O.cleanup_after, O.object_key
|
|
328
|
+
FOR UPDATE OF O SKIP LOCKED
|
|
329
|
+
LIMIT least(greatest(p_limit, 0), 1000)
|
|
330
|
+
), claimed AS (
|
|
331
|
+
UPDATE %1$I.transcription_recording_objects O
|
|
332
|
+
SET cleanup_claim_id = gen_random_uuid(), cleanup_claimed_at = clock_timestamp()
|
|
333
|
+
FROM candidates C
|
|
334
|
+
WHERE O.object_key = C.object_key
|
|
335
|
+
RETURNING
|
|
336
|
+
O.account_id,
|
|
337
|
+
O.workspace_id,
|
|
338
|
+
O.subject_id,
|
|
339
|
+
O.recording_id,
|
|
340
|
+
O.object_key,
|
|
341
|
+
O.cleanup_claim_id
|
|
342
|
+
)
|
|
343
|
+
SELECT
|
|
344
|
+
C.account_id,
|
|
345
|
+
C.workspace_id,
|
|
346
|
+
C.subject_id,
|
|
347
|
+
C.recording_id,
|
|
348
|
+
C.object_key,
|
|
349
|
+
C.cleanup_claim_id
|
|
350
|
+
FROM claimed C, (SELECT count(*) FROM expired_recordings) AS fence;
|
|
351
|
+
$function$;
|
|
352
|
+
$ddl$, data_schema);
|
|
353
|
+
|
|
354
|
+
-- Once every object for an expired recording has been confirmed deleted,
|
|
355
|
+
-- purge the remaining manifest, chunk/segment metadata, provider pin, and
|
|
356
|
+
-- transcript. Provider deletion settles one object at a time first, so
|
|
357
|
+
-- metadata is never removed while an opaque object key still needs retryable
|
|
358
|
+
-- cleanup.
|
|
359
|
+
EXECUTE format($ddl$
|
|
360
|
+
CREATE OR REPLACE FUNCTION opengeni_private.purge_expired_transcription_recordings(
|
|
361
|
+
p_grace_ms bigint,
|
|
362
|
+
p_limit integer
|
|
363
|
+
)
|
|
364
|
+
RETURNS integer
|
|
365
|
+
LANGUAGE plpgsql
|
|
366
|
+
SECURITY DEFINER
|
|
367
|
+
STRICT
|
|
368
|
+
SET search_path = pg_catalog
|
|
369
|
+
AS $function$
|
|
370
|
+
DECLARE
|
|
371
|
+
v_recording_id uuid;
|
|
372
|
+
v_purged integer := 0;
|
|
373
|
+
BEGIN
|
|
374
|
+
IF p_grace_ms < 0 OR p_limit <= 0 THEN
|
|
375
|
+
RAISE EXCEPTION 'invalid transcription recording purge bounds';
|
|
376
|
+
END IF;
|
|
377
|
+
|
|
378
|
+
FOR v_recording_id IN
|
|
379
|
+
SELECT R.id
|
|
380
|
+
FROM %1$I.transcription_recordings R
|
|
381
|
+
WHERE R.expires_at <=
|
|
382
|
+
clock_timestamp() - p_grace_ms * interval '1 millisecond'
|
|
383
|
+
AND NOT EXISTS (
|
|
384
|
+
SELECT 1
|
|
385
|
+
FROM %1$I.transcription_recording_objects O
|
|
386
|
+
WHERE O.recording_id = R.id
|
|
387
|
+
AND O.cleaned_at IS NULL
|
|
388
|
+
)
|
|
389
|
+
ORDER BY R.expires_at, R.id
|
|
390
|
+
FOR UPDATE OF R SKIP LOCKED
|
|
391
|
+
LIMIT least(p_limit, 1000)
|
|
392
|
+
LOOP
|
|
393
|
+
DELETE FROM %1$I.transcription_recording_chunks
|
|
394
|
+
WHERE recording_id = v_recording_id;
|
|
395
|
+
DELETE FROM %1$I.transcription_recording_segments
|
|
396
|
+
WHERE recording_id = v_recording_id;
|
|
397
|
+
DELETE FROM %1$I.transcription_recording_objects
|
|
398
|
+
WHERE recording_id = v_recording_id;
|
|
399
|
+
DELETE FROM %1$I.transcription_recordings
|
|
400
|
+
WHERE id = v_recording_id;
|
|
401
|
+
v_purged := v_purged + 1;
|
|
402
|
+
END LOOP;
|
|
403
|
+
|
|
404
|
+
RETURN v_purged;
|
|
405
|
+
END;
|
|
406
|
+
$function$;
|
|
407
|
+
$ddl$, data_schema);
|
|
408
|
+
END
|
|
409
|
+
$privileged_functions$;
|
|
410
|
+
|
|
411
|
+
REVOKE ALL ON FUNCTION
|
|
412
|
+
opengeni_private.claim_due_transcription_recording_object_cleanup(bigint, bigint, integer)
|
|
413
|
+
FROM PUBLIC;
|
|
414
|
+
|
|
415
|
+
REVOKE ALL ON FUNCTION
|
|
416
|
+
opengeni_private.purge_expired_transcription_recordings(bigint, integer)
|
|
417
|
+
FROM PUBLIC;
|
|
418
|
+
|
|
419
|
+
DO $$
|
|
420
|
+
BEGIN
|
|
421
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
422
|
+
GRANT EXECUTE ON FUNCTION
|
|
423
|
+
opengeni_private.claim_due_transcription_recording_object_cleanup(bigint, bigint, integer)
|
|
424
|
+
TO opengeni_app;
|
|
425
|
+
GRANT EXECUTE ON FUNCTION
|
|
426
|
+
opengeni_private.purge_expired_transcription_recordings(bigint, integer)
|
|
427
|
+
TO opengeni_app;
|
|
428
|
+
END IF;
|
|
429
|
+
END $$;
|
|
430
|
+
|
|
431
|
+
CREATE POLICY transcription_recording_segments_subject_isolation
|
|
432
|
+
ON "transcription_recording_segments"
|
|
433
|
+
USING (
|
|
434
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
435
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
436
|
+
)
|
|
437
|
+
WITH CHECK (
|
|
438
|
+
opengeni_private.workspace_rls_visible("account_id", "workspace_id")
|
|
439
|
+
AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
|
|
440
|
+
);
|