@antiphony/shared 0.4.0 → 0.5.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.
@@ -61,21 +61,129 @@ var FirestoreTimestampSchema = zod.z.union([
61
61
  return date;
62
62
  });
63
63
  var ProcessingStageStatusSchema = zod.z.enum(["pending", "ready", "failed", "skipped"]);
64
+ var PROCESSING_STAGES = ["denoise", "trim", "transcribe", "waveform"];
65
+ var ProcessingStageSchema = zod.z.enum(PROCESSING_STAGES);
66
+ var BYTE_MUTATING_STAGES = ["denoise", "trim"];
67
+ var DERIVED_STAGES = ["transcribe", "waveform"];
64
68
  var ProcessingRequestSchema = zod.z.object({
65
69
  transcribe: zod.z.boolean().optional(),
66
- denoise: zod.z.boolean().optional()
70
+ denoise: zod.z.boolean().optional(),
71
+ trim: zod.z.boolean().optional(),
72
+ waveform: zod.z.boolean().optional(),
73
+ /**
74
+ * Whether a completed byte-mutating stage should invalidate and recompute
75
+ * the derived artifacts that describe the old audio. Defaults to **true**
76
+ * — a transcript of superseded audio is wrong, not merely stale.
77
+ *
78
+ * `false` opts out, for an app that would rather keep the existing
79
+ * transcript than pay to regenerate it. It does NOT name a stage, so a
80
+ * request carrying only `reprocess` requests no work.
81
+ */
82
+ reprocess: zod.z.boolean().optional()
67
83
  });
68
- var ProcessingStateSchema = zod.z.object({
84
+ var ProcessingStageMapSchema = zod.z.object({
69
85
  transcribe: ProcessingStageStatusSchema.optional(),
70
86
  denoise: ProcessingStageStatusSchema.optional(),
71
- /** Content CID of the denoised audio variant, once `denoise === 'ready'`. */
72
- denoisedBlobCid: zod.z.string().optional(),
73
- updatedAt: FirestoreTimestampSchema
87
+ trim: ProcessingStageStatusSchema.optional(),
88
+ waveform: ProcessingStageStatusSchema.optional()
74
89
  });
75
- var ProcessingViewSchema = zod.z.object({
76
- transcribe: ProcessingStageStatusSchema.optional(),
77
- denoise: ProcessingStageStatusSchema.optional()
90
+ var ResolvedProcessingSchema = ProcessingStageMapSchema.extend({
91
+ reprocess: zod.z.boolean().optional()
92
+ });
93
+ var ProcessingStateSchema = ResolvedProcessingSchema.extend({
94
+ /**
95
+ * Content CID of the processed audio variant — the composed output of every
96
+ * byte-mutating stage that has completed. The record's own
97
+ * `embed.audio.ref.$link` stays the ORIGINAL CID (immutable content
98
+ * address); only the read-time view swaps playback to this variant.
99
+ */
100
+ processedBlobCid: zod.z.string().optional(),
101
+ /**
102
+ * MIME type of the processed variant. Present because providers may
103
+ * TRANSCODE — the ElevenLabs Voice Isolator returns MP3 regardless of what
104
+ * it is given — so the variant's type cannot be assumed to match
105
+ * `embed.audio.mimeType`. Anything reading the variant's bytes must use
106
+ * this, not the embed's.
107
+ */
108
+ processedMimeType: zod.z.string().optional(),
109
+ /**
110
+ * Duration of the processed variant, when a byte-mutating stage changed it
111
+ * (i.e. trim). Absent when the variant's duration matches the original.
112
+ */
113
+ processedDurationMs: zod.z.number().int().min(0).optional(),
114
+ /**
115
+ * Which denoiser produced the variant's denoise contribution — provenance,
116
+ * the counterpart to a transcript record's `model`.
117
+ *
118
+ * Lives here because a cleaned variant, unlike a transcript, has no record
119
+ * of its own to carry it: it is a blob CID on this state. Without it,
120
+ * changing denoisers leaves no way to tell which variants predate the
121
+ * switch, so nothing can identify what to re-run.
122
+ *
123
+ * Named for the STAGE, not the variant (`processedModel`), because it
124
+ * describes one link of the byte-mutating chain rather than the composed
125
+ * artifact. Trim contributes to the same variant and has no model, and a
126
+ * later external link would want its own field rather than to overwrite
127
+ * this one.
128
+ *
129
+ * Written on every successful denoise, never cleared — it moves with
130
+ * `processedBlobCid`, which is only ever set, never reset. A denoise that
131
+ * FAILS leaves both alone, which is correct: the variant still holds the
132
+ * previous denoiser's output, so the previous model still describes it.
133
+ *
134
+ * Internal, like the other variant fields — `toProcessingView` projects
135
+ * stages only, so this never reaches a client.
136
+ */
137
+ denoiseModel: zod.z.string().optional(),
138
+ /**
139
+ * Peaks for the processed variant, once the `waveform` stage completes.
140
+ * Same normalization and bounds as `embed.waveform` (0–100, max 1000), so
141
+ * a view can never carry a larger payload than the record allows.
142
+ */
143
+ waveformPeaks: zod.z.array(zod.z.number().int().min(0).max(100)).max(1e3).optional(),
144
+ /**
145
+ * When the current runner's exclusive claim on this post expires.
146
+ *
147
+ * Queue delivery is at-least-once, so the same job can arrive twice and
148
+ * run CONCURRENTLY. `process()` is idempotent under sequential retry — it
149
+ * acts on `pending` and re-does nothing already settled — but two passes
150
+ * interleaved is a different failure: both read the same `pending` state,
151
+ * both bill the provider for the same stage, and both write
152
+ * `processedBlobCid`, so the surviving variant is whichever finished last
153
+ * and the other's blob is orphaned.
154
+ *
155
+ * A runner claims this field transactionally before doing any work and
156
+ * clears it when finished; a second runner finding it unexpired declines
157
+ * and returns. It is an EXPIRY, not a boolean lock, because the holder can
158
+ * die mid-run (instance recycled, process killed) with no chance to
159
+ * release — a plain flag would strand the post permanently, where a lapsed
160
+ * lease lets the next delivery pick it up.
161
+ *
162
+ * Internal, like the variant fields above: `toProcessingView` projects
163
+ * stages only, so this never reaches a client.
164
+ */
165
+ leaseUntil: FirestoreTimestampSchema.optional(),
166
+ updatedAt: FirestoreTimestampSchema
78
167
  });
168
+ var ProcessingViewSchema = ProcessingStageMapSchema;
169
+ function toProcessingView(state) {
170
+ const view = {};
171
+ for (const stage of PROCESSING_STAGES) {
172
+ if (state[stage] !== void 0) view[stage] = state[stage];
173
+ }
174
+ return view;
175
+ }
176
+ function resolveAudioVariant(canonical, state) {
177
+ var _a;
178
+ if (!state) return canonical;
179
+ const hasVariant = state.processedBlobCid !== void 0;
180
+ const peaksAreCurrent = state.waveform === "ready" && state.waveformPeaks !== void 0;
181
+ return {
182
+ blobCid: hasVariant ? state.processedBlobCid : canonical.blobCid,
183
+ durationMs: hasVariant ? (_a = state.processedDurationMs) != null ? _a : canonical.durationMs : canonical.durationMs,
184
+ waveform: peaksAreCurrent ? state.waveformPeaks : canonical.waveform
185
+ };
186
+ }
79
187
 
80
188
  // types/audio.ts
81
189
  var StrongRefSchema = zod.z.object({
@@ -111,20 +219,34 @@ var TimedTranscriptSchema = zod.z.object({
111
219
  });
112
220
  var AudioEmbedViewSchema = zod.z.object({
113
221
  $type: zod.z.literal("dev.antiphony.embed.audio#view"),
222
+ /**
223
+ * `url`, `durationMs` and `waveform` are RESOLVED, not copied: once
224
+ * processing has produced an audio variant they describe that variant
225
+ * rather than the bytes the client uploaded. They always agree with one
226
+ * another — a duration and a set of peaks are only meaningful against the
227
+ * audio `url` actually points at.
228
+ *
229
+ * A client that stored `durationMs` at upload time should therefore expect
230
+ * it to change (trim removes leading/trailing silence), and should render
231
+ * these three as a set rather than caching them independently. The record's
232
+ * originals are immutable and unaffected; this is a read-time resolution.
233
+ */
114
234
  url: zod.z.string().url(),
115
235
  durationMs: zod.z.number().int().min(0).optional(),
116
- // `alt`/`waveform` are copied from the stored embed; keep the same bounds
117
- // so a view can never carry a larger payload than the record allows.
236
+ // `alt` is copied from the stored embed; keep the same bounds so a view can
237
+ // never carry a larger payload than the record allows.
118
238
  alt: zod.z.string().max(1e4).optional(),
119
239
  waveform: zod.z.array(zod.z.number().int().min(0).max(100)).max(1e3).optional(),
120
240
  /** Lifted from the transcript enrichment record; absent until transcription completes. */
121
241
  transcript: TimedTranscriptSchema.optional(),
122
242
  /**
123
- * Per-stage audio-processing status (transcribe / denoise), when the app
124
- * opted into processing on create. Absent otherwise. A `pending` stage
125
- * means the client should poll (or re-render) for the result. When
126
- * `denoise === 'ready'`, `url` above already resolves to the cleaned
127
- * audio variant. See `types/processing.ts`.
243
+ * Per-stage audio-processing status (denoise / trim / transcribe /
244
+ * waveform), when the app opted into processing. Absent otherwise. A
245
+ * `pending` stage means the client should poll (or re-render) for the
246
+ * result including a stage that returns to `pending` after having been
247
+ * `ready`, which is how a recompute surfaces. Once a byte-mutating stage
248
+ * completes, `url`/`durationMs`/`waveform` above already resolve to the
249
+ * processed audio variant. See `types/processing.ts`.
128
250
  */
129
251
  processing: ProcessingViewSchema.optional()
130
252
  });
@@ -228,10 +350,15 @@ var ViewerStateSchema = zod.z.object({
228
350
  replyDisabledReason: zod.z.enum(["unauthenticated", "not_a_participant"]).optional()
229
351
  });
230
352
  var PostRecordPublicSchema = zod.z.object({
231
- text: zod.z.string(),
232
- title: zod.z.string().optional(),
353
+ // `text`, `title` and `langs` keep the record's bounds, for the same reason
354
+ // `AudioEmbedViewSchema.alt` does: a view must never be able to carry a
355
+ // larger payload than the record it projects. The write path already
356
+ // enforces these, so no stored post can exceed them — stating them here
357
+ // keeps the published contract honest rather than adding a new constraint.
358
+ text: zod.z.string().max(3e3),
359
+ title: zod.z.string().max(3e3).optional(),
233
360
  reply: ReplyRefSchema.optional(),
234
- langs: zod.z.array(zod.z.string()).optional(),
361
+ langs: zod.z.array(zod.z.string()).max(3).optional(),
235
362
  selfLabels: zod.z.array(zod.z.string()).optional(),
236
363
  createdAt: FirestoreTimestampSchema
237
364
  });
@@ -266,9 +393,11 @@ var CreateAudioPostRequestSchema = zod.z.object({
266
393
  /** Author self-label values (content warnings). */
267
394
  selfLabels: zod.z.array(zod.z.string()).optional(),
268
395
  /**
269
- * Opt-in audio processing for this post's audio (transcribe / denoise).
270
- * Both default off. Stages the deployment can't provide come back marked
271
- * `skipped` on the view rather than failing the create. See
396
+ * Opt-in audio processing for this post's audio (denoise / trim /
397
+ * transcribe / waveform). All default off. Stages the deployment can't
398
+ * provide come back marked `skipped` on the view rather than failing the
399
+ * create. A multi-stage request runs denoise → trim → (transcribe,
400
+ * waveform); request stages individually to override that order. See
272
401
  * `types/processing.ts`.
273
402
  */
274
403
  processing: ProcessingRequestSchema.optional()
@@ -301,6 +430,15 @@ var EMBED_NSID = {
301
430
  Audio: "dev.antiphony.embed.audio",
302
431
  RecordWithAudio: "dev.antiphony.embed.recordWithAudio"
303
432
  };
433
+ var XRPC_NSID = {
434
+ // Queries (GET).
435
+ GetPost: "dev.antiphony.audio.getPost",
436
+ GetThread: "dev.antiphony.audio.getThread",
437
+ GetPlaybackUrl: "dev.antiphony.audio.getPlaybackUrl",
438
+ // Procedures (POST).
439
+ CreatePost: "dev.antiphony.audio.createPost",
440
+ ReprocessPost: "dev.antiphony.audio.reprocessPost"
441
+ };
304
442
  var COLLECTIONS = {
305
443
  // One post collection + the transcript enrichment namespace.
306
444
  [NSID.AudioPost]: "posts",
@@ -401,23 +539,29 @@ exports.AudioEmbedSchema = AudioEmbedSchema;
401
539
  exports.AudioEmbedViewSchema = AudioEmbedViewSchema;
402
540
  exports.AudioPostRecordSchema = AudioPostRecordSchema;
403
541
  exports.AudioPostViewSchema = AudioPostViewSchema;
542
+ exports.BYTE_MUTATING_STAGES = BYTE_MUTATING_STAGES;
404
543
  exports.BlobRefSchema = BlobRefSchema;
405
544
  exports.COLLECTIONS = COLLECTIONS;
406
545
  exports.ConflictError = ConflictError;
407
546
  exports.CreateAudioPostRequestSchema = CreateAudioPostRequestSchema;
547
+ exports.DERIVED_STAGES = DERIVED_STAGES;
408
548
  exports.EMBED_NSID = EMBED_NSID;
409
549
  exports.FirestoreTimestampSchema = FirestoreTimestampSchema;
410
550
  exports.ForbiddenError = ForbiddenError;
411
551
  exports.NSID = NSID;
412
552
  exports.NotFoundError = NotFoundError;
553
+ exports.PROCESSING_STAGES = PROCESSING_STAGES;
413
554
  exports.PatchAudioPostRequestSchema = PatchAudioPostRequestSchema;
414
555
  exports.PostRecordPublicSchema = PostRecordPublicSchema;
415
556
  exports.ProcessingRequestSchema = ProcessingRequestSchema;
557
+ exports.ProcessingStageMapSchema = ProcessingStageMapSchema;
558
+ exports.ProcessingStageSchema = ProcessingStageSchema;
416
559
  exports.ProcessingStageStatusSchema = ProcessingStageStatusSchema;
417
560
  exports.ProcessingStateSchema = ProcessingStateSchema;
418
561
  exports.ProcessingViewSchema = ProcessingViewSchema;
419
562
  exports.RateLimitError = RateLimitError;
420
563
  exports.ReplyRefSchema = ReplyRefSchema;
564
+ exports.ResolvedProcessingSchema = ResolvedProcessingSchema;
421
565
  exports.ServiceError = ServiceError;
422
566
  exports.StrongRefSchema = StrongRefSchema;
423
567
  exports.TimedTranscriptSchema = TimedTranscriptSchema;
@@ -426,6 +570,9 @@ exports.TranscriptSegmentSchema = TranscriptSegmentSchema;
426
570
  exports.UnauthorizedError = UnauthorizedError;
427
571
  exports.ValidationError = ValidationError;
428
572
  exports.ViewerStateSchema = ViewerStateSchema;
573
+ exports.XRPC_NSID = XRPC_NSID;
429
574
  exports.buildReportedErrorEvent = buildReportedErrorEvent;
430
575
  exports.isFirestoreTimestamp = isFirestoreTimestamp;
431
576
  exports.reportError = reportError;
577
+ exports.resolveAudioVariant = resolveAudioVariant;
578
+ exports.toProcessingView = toProcessingView;
@@ -1,93 +1,10 @@
1
1
  export { CreateAudioPostRequest, CreateAudioPostRequestSchema, PatchAudioPostRequest, PatchAudioPostRequestSchema } from './api-codecs.cjs';
2
2
  export { FirestoreTimestamp, FirestoreTimestampSchema } from './types/records.cjs';
3
3
  export { ActorProfileRecord, ActorProfileRecordSchema, AudioEmbed, AudioEmbedSchema, AudioEmbedView, AudioEmbedViewSchema, AudioPostRecord, AudioPostRecordSchema, AudioPostView, AudioPostViewSchema, PostRecordPublic, PostRecordPublicSchema, ReplyRef, ReplyRefSchema, StrongRef, StrongRefSchema, TimedTranscript, TimedTranscriptSchema, TranscriptEnrichmentRecord, TranscriptEnrichmentRecordSchema, TranscriptSegment, TranscriptSegmentSchema, ViewerState, ViewerStateSchema } from './types/audio.cjs';
4
- import { z } from 'zod';
4
+ export { BYTE_MUTATING_STAGES, CanonicalAudioFields, DERIVED_STAGES, PROCESSING_STAGES, ProcessingRequest, ProcessingRequestSchema, ProcessingStage, ProcessingStageMap, ProcessingStageMapSchema, ProcessingStageSchema, ProcessingStageStatus, ProcessingStageStatusSchema, ProcessingState, ProcessingStateSchema, ProcessingView, ProcessingViewSchema, ResolvedProcessing, ResolvedProcessingSchema, resolveAudioVariant, toProcessingView } from './types/processing.cjs';
5
5
  export { BlobRef, BlobRefSchema } from './types/blob.cjs';
6
- export { COLLECTIONS, EMBED_NSID, NSID, NsidValue, StoredNsidValue } from './nsid.cjs';
6
+ export { COLLECTIONS, EMBED_NSID, NSID, NsidValue, StoredNsidValue, XRPC_NSID, XrpcNsidValue } from './nsid.cjs';
7
7
  export { ConflictError, ForbiddenError, NotFoundError, RateLimitError, ServiceError, UnauthorizedError, ValidationError } from './errors/index.cjs';
8
8
  export { isFirestoreTimestamp } from './utils/index.cjs';
9
9
  export { ReportedErrorEvent, buildReportedErrorEvent, reportError } from './observability/report-error.cjs';
10
-
11
- /**
12
- * Audio hygiene / enrichment processing (B5).
13
- *
14
- * Antiphony can, when the calling app opts in, run two pieces of audio
15
- * processing on a post's audio: **transcription** (machine transcript, the
16
- * `dev.antiphony.audio.transcript` enrichment) and **denoise** (a cleaned
17
- * audio variant for playback). Both are OFF by default — the app asks for
18
- * them per post via `CreateAudioPostRequest.processing`.
19
- *
20
- * The work runs asynchronously (outside the create request), so a post
21
- * carries a mutable `processing` state that starts `pending` and settles to
22
- * `ready`/`failed`/`skipped` per stage. That state is a storage-layer field —
23
- * it is NOT part of the canonical lexicon record and never enters the record
24
- * CID (like `kind` and `threadParticipants`), so processing can update it
25
- * without changing the post's content address.
26
- */
27
- /**
28
- * Per-stage status.
29
- * - `pending` — requested, not yet done (the worker acts on these).
30
- * - `ready` — completed.
31
- * - `failed` — attempted and errored.
32
- * - `skipped` — requested but this deployment has no provider for it.
33
- */
34
- declare const ProcessingStageStatusSchema: z.ZodEnum<["pending", "ready", "failed", "skipped"]>;
35
- type ProcessingStageStatus = z.infer<typeof ProcessingStageStatusSchema>;
36
- /**
37
- * What the calling app opts into, on `CreateAudioPostRequest`. Both default
38
- * off; only `true` values request a stage.
39
- */
40
- declare const ProcessingRequestSchema: z.ZodObject<{
41
- transcribe: z.ZodOptional<z.ZodBoolean>;
42
- denoise: z.ZodOptional<z.ZodBoolean>;
43
- }, "strip", z.ZodTypeAny, {
44
- transcribe?: boolean | undefined;
45
- denoise?: boolean | undefined;
46
- }, {
47
- transcribe?: boolean | undefined;
48
- denoise?: boolean | undefined;
49
- }>;
50
- type ProcessingRequest = z.infer<typeof ProcessingRequestSchema>;
51
- /**
52
- * Stored processing state on the post record (storage-layer; not in the CID).
53
- * A stage key is present iff that stage was requested. `denoisedBlobCid`
54
- * points at the cleaned audio variant once denoise completes — the record's
55
- * own `embed.audio.ref.$link` stays the ORIGINAL CID (immutable content
56
- * address); only the read-time view swaps playback to the cleaned variant.
57
- */
58
- declare const ProcessingStateSchema: z.ZodObject<{
59
- transcribe: z.ZodOptional<z.ZodEnum<["pending", "ready", "failed", "skipped"]>>;
60
- denoise: z.ZodOptional<z.ZodEnum<["pending", "ready", "failed", "skipped"]>>;
61
- /** Content CID of the denoised audio variant, once `denoise === 'ready'`. */
62
- denoisedBlobCid: z.ZodOptional<z.ZodString>;
63
- updatedAt: z.ZodEffects<z.ZodUnion<[z.ZodType<unknown, z.ZodTypeDef, unknown>, z.ZodString, z.ZodNumber, z.ZodDate]>, Date, unknown>;
64
- }, "strip", z.ZodTypeAny, {
65
- updatedAt: Date;
66
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
67
- denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
68
- denoisedBlobCid?: string | undefined;
69
- }, {
70
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
71
- denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
72
- denoisedBlobCid?: string | undefined;
73
- updatedAt?: unknown;
74
- }>;
75
- type ProcessingState = z.infer<typeof ProcessingStateSchema>;
76
- /**
77
- * The processing status surfaced on the hydrated view — the per-stage status
78
- * only (no internal storage fields). Absent when no processing was requested.
79
- * Clients treat any `pending` stage as "still working".
80
- */
81
- declare const ProcessingViewSchema: z.ZodObject<{
82
- transcribe: z.ZodOptional<z.ZodEnum<["pending", "ready", "failed", "skipped"]>>;
83
- denoise: z.ZodOptional<z.ZodEnum<["pending", "ready", "failed", "skipped"]>>;
84
- }, "strip", z.ZodTypeAny, {
85
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
86
- denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
87
- }, {
88
- transcribe?: "pending" | "ready" | "failed" | "skipped" | undefined;
89
- denoise?: "pending" | "ready" | "failed" | "skipped" | undefined;
90
- }>;
91
- type ProcessingView = z.infer<typeof ProcessingViewSchema>;
92
-
93
- export { type ProcessingRequest, ProcessingRequestSchema, type ProcessingStageStatus, ProcessingStageStatusSchema, type ProcessingState, ProcessingStateSchema, type ProcessingView, ProcessingViewSchema };
10
+ import 'zod';
package/dist/cjs/nsid.cjs CHANGED
@@ -16,6 +16,15 @@ var EMBED_NSID = {
16
16
  Audio: "dev.antiphony.embed.audio",
17
17
  RecordWithAudio: "dev.antiphony.embed.recordWithAudio"
18
18
  };
19
+ var XRPC_NSID = {
20
+ // Queries (GET).
21
+ GetPost: "dev.antiphony.audio.getPost",
22
+ GetThread: "dev.antiphony.audio.getThread",
23
+ GetPlaybackUrl: "dev.antiphony.audio.getPlaybackUrl",
24
+ // Procedures (POST).
25
+ CreatePost: "dev.antiphony.audio.createPost",
26
+ ReprocessPost: "dev.antiphony.audio.reprocessPost"
27
+ };
19
28
  var COLLECTIONS = {
20
29
  // One post collection + the transcript enrichment namespace.
21
30
  [NSID.AudioPost]: "posts",
@@ -25,3 +34,4 @@ var COLLECTIONS = {
25
34
  exports.COLLECTIONS = COLLECTIONS;
26
35
  exports.EMBED_NSID = EMBED_NSID;
27
36
  exports.NSID = NSID;
37
+ exports.XRPC_NSID = XRPC_NSID;
@@ -23,6 +23,30 @@ declare const EMBED_NSID: {
23
23
  readonly Audio: "dev.antiphony.embed.audio";
24
24
  readonly RecordWithAudio: "dev.antiphony.embed.recordWithAudio";
25
25
  };
26
+ /**
27
+ * XRPC method NSIDs — the `/xrpc/<nsid>` surface (see
28
+ * specs/xrpc-and-atproto-lex-strategy.md).
29
+ *
30
+ * These are **siblings** of the record NSIDs in `NSID`, not children of them.
31
+ * A method shares the authority segment (`dev.antiphony.audio`) with the record
32
+ * but never nests under the record's own name: the record is
33
+ * `dev.antiphony.audio.post` and the query that fetches it is
34
+ * `dev.antiphony.audio.getPost`. Deriving one by appending to the other
35
+ * produces `dev.antiphony.audio.post.getPost`, a different and undefined
36
+ * namespace — hence a separate map rather than a helper over `NSID`.
37
+ *
38
+ * Queries are `GET`, procedures are `POST`; the grouping below follows that
39
+ * split because it is also the auth split (procedures always require an acting
40
+ * actor, queries may be viewer-less).
41
+ */
42
+ declare const XRPC_NSID: {
43
+ readonly GetPost: "dev.antiphony.audio.getPost";
44
+ readonly GetThread: "dev.antiphony.audio.getThread";
45
+ readonly GetPlaybackUrl: "dev.antiphony.audio.getPlaybackUrl";
46
+ readonly CreatePost: "dev.antiphony.audio.createPost";
47
+ readonly ReprocessPost: "dev.antiphony.audio.reprocessPost";
48
+ };
49
+ type XrpcNsidValue = typeof XRPC_NSID[keyof typeof XRPC_NSID];
26
50
  /**
27
51
  * Maps the STORED AT Protocol record-type NSIDs to Firestore collection
28
52
  * names. When migrating to a PDS, this mapping becomes the adapter layer.
@@ -30,4 +54,4 @@ declare const EMBED_NSID: {
30
54
  */
31
55
  declare const COLLECTIONS: Record<StoredNsidValue, string>;
32
56
 
33
- export { COLLECTIONS, EMBED_NSID, NSID, type NsidValue, type StoredNsidValue };
57
+ export { COLLECTIONS, EMBED_NSID, NSID, type NsidValue, type StoredNsidValue, XRPC_NSID, type XrpcNsidValue };
@@ -46,21 +46,109 @@ var FirestoreTimestampSchema = zod.z.union([
46
46
  return date;
47
47
  });
48
48
  var ProcessingStageStatusSchema = zod.z.enum(["pending", "ready", "failed", "skipped"]);
49
+ var PROCESSING_STAGES = ["denoise", "trim", "transcribe", "waveform"];
50
+ zod.z.enum(PROCESSING_STAGES);
49
51
  zod.z.object({
50
52
  transcribe: zod.z.boolean().optional(),
51
- denoise: zod.z.boolean().optional()
53
+ denoise: zod.z.boolean().optional(),
54
+ trim: zod.z.boolean().optional(),
55
+ waveform: zod.z.boolean().optional(),
56
+ /**
57
+ * Whether a completed byte-mutating stage should invalidate and recompute
58
+ * the derived artifacts that describe the old audio. Defaults to **true**
59
+ * — a transcript of superseded audio is wrong, not merely stale.
60
+ *
61
+ * `false` opts out, for an app that would rather keep the existing
62
+ * transcript than pay to regenerate it. It does NOT name a stage, so a
63
+ * request carrying only `reprocess` requests no work.
64
+ */
65
+ reprocess: zod.z.boolean().optional()
52
66
  });
53
- var ProcessingStateSchema = zod.z.object({
67
+ var ProcessingStageMapSchema = zod.z.object({
54
68
  transcribe: ProcessingStageStatusSchema.optional(),
55
69
  denoise: ProcessingStageStatusSchema.optional(),
56
- /** Content CID of the denoised audio variant, once `denoise === 'ready'`. */
57
- denoisedBlobCid: zod.z.string().optional(),
58
- updatedAt: FirestoreTimestampSchema
70
+ trim: ProcessingStageStatusSchema.optional(),
71
+ waveform: ProcessingStageStatusSchema.optional()
59
72
  });
60
- var ProcessingViewSchema = zod.z.object({
61
- transcribe: ProcessingStageStatusSchema.optional(),
62
- denoise: ProcessingStageStatusSchema.optional()
73
+ var ResolvedProcessingSchema = ProcessingStageMapSchema.extend({
74
+ reprocess: zod.z.boolean().optional()
63
75
  });
76
+ var ProcessingStateSchema = ResolvedProcessingSchema.extend({
77
+ /**
78
+ * Content CID of the processed audio variant — the composed output of every
79
+ * byte-mutating stage that has completed. The record's own
80
+ * `embed.audio.ref.$link` stays the ORIGINAL CID (immutable content
81
+ * address); only the read-time view swaps playback to this variant.
82
+ */
83
+ processedBlobCid: zod.z.string().optional(),
84
+ /**
85
+ * MIME type of the processed variant. Present because providers may
86
+ * TRANSCODE — the ElevenLabs Voice Isolator returns MP3 regardless of what
87
+ * it is given — so the variant's type cannot be assumed to match
88
+ * `embed.audio.mimeType`. Anything reading the variant's bytes must use
89
+ * this, not the embed's.
90
+ */
91
+ processedMimeType: zod.z.string().optional(),
92
+ /**
93
+ * Duration of the processed variant, when a byte-mutating stage changed it
94
+ * (i.e. trim). Absent when the variant's duration matches the original.
95
+ */
96
+ processedDurationMs: zod.z.number().int().min(0).optional(),
97
+ /**
98
+ * Which denoiser produced the variant's denoise contribution — provenance,
99
+ * the counterpart to a transcript record's `model`.
100
+ *
101
+ * Lives here because a cleaned variant, unlike a transcript, has no record
102
+ * of its own to carry it: it is a blob CID on this state. Without it,
103
+ * changing denoisers leaves no way to tell which variants predate the
104
+ * switch, so nothing can identify what to re-run.
105
+ *
106
+ * Named for the STAGE, not the variant (`processedModel`), because it
107
+ * describes one link of the byte-mutating chain rather than the composed
108
+ * artifact. Trim contributes to the same variant and has no model, and a
109
+ * later external link would want its own field rather than to overwrite
110
+ * this one.
111
+ *
112
+ * Written on every successful denoise, never cleared — it moves with
113
+ * `processedBlobCid`, which is only ever set, never reset. A denoise that
114
+ * FAILS leaves both alone, which is correct: the variant still holds the
115
+ * previous denoiser's output, so the previous model still describes it.
116
+ *
117
+ * Internal, like the other variant fields — `toProcessingView` projects
118
+ * stages only, so this never reaches a client.
119
+ */
120
+ denoiseModel: zod.z.string().optional(),
121
+ /**
122
+ * Peaks for the processed variant, once the `waveform` stage completes.
123
+ * Same normalization and bounds as `embed.waveform` (0–100, max 1000), so
124
+ * a view can never carry a larger payload than the record allows.
125
+ */
126
+ waveformPeaks: zod.z.array(zod.z.number().int().min(0).max(100)).max(1e3).optional(),
127
+ /**
128
+ * When the current runner's exclusive claim on this post expires.
129
+ *
130
+ * Queue delivery is at-least-once, so the same job can arrive twice and
131
+ * run CONCURRENTLY. `process()` is idempotent under sequential retry — it
132
+ * acts on `pending` and re-does nothing already settled — but two passes
133
+ * interleaved is a different failure: both read the same `pending` state,
134
+ * both bill the provider for the same stage, and both write
135
+ * `processedBlobCid`, so the surviving variant is whichever finished last
136
+ * and the other's blob is orphaned.
137
+ *
138
+ * A runner claims this field transactionally before doing any work and
139
+ * clears it when finished; a second runner finding it unexpired declines
140
+ * and returns. It is an EXPIRY, not a boolean lock, because the holder can
141
+ * die mid-run (instance recycled, process killed) with no chance to
142
+ * release — a plain flag would strand the post permanently, where a lapsed
143
+ * lease lets the next delivery pick it up.
144
+ *
145
+ * Internal, like the variant fields above: `toProcessingView` projects
146
+ * stages only, so this never reaches a client.
147
+ */
148
+ leaseUntil: FirestoreTimestampSchema.optional(),
149
+ updatedAt: FirestoreTimestampSchema
150
+ });
151
+ var ProcessingViewSchema = ProcessingStageMapSchema;
64
152
 
65
153
  // types/audio.ts
66
154
  var StrongRefSchema = zod.z.object({
@@ -96,20 +184,34 @@ var TimedTranscriptSchema = zod.z.object({
96
184
  });
97
185
  var AudioEmbedViewSchema = zod.z.object({
98
186
  $type: zod.z.literal("dev.antiphony.embed.audio#view"),
187
+ /**
188
+ * `url`, `durationMs` and `waveform` are RESOLVED, not copied: once
189
+ * processing has produced an audio variant they describe that variant
190
+ * rather than the bytes the client uploaded. They always agree with one
191
+ * another — a duration and a set of peaks are only meaningful against the
192
+ * audio `url` actually points at.
193
+ *
194
+ * A client that stored `durationMs` at upload time should therefore expect
195
+ * it to change (trim removes leading/trailing silence), and should render
196
+ * these three as a set rather than caching them independently. The record's
197
+ * originals are immutable and unaffected; this is a read-time resolution.
198
+ */
99
199
  url: zod.z.string().url(),
100
200
  durationMs: zod.z.number().int().min(0).optional(),
101
- // `alt`/`waveform` are copied from the stored embed; keep the same bounds
102
- // so a view can never carry a larger payload than the record allows.
201
+ // `alt` is copied from the stored embed; keep the same bounds so a view can
202
+ // never carry a larger payload than the record allows.
103
203
  alt: zod.z.string().max(1e4).optional(),
104
204
  waveform: zod.z.array(zod.z.number().int().min(0).max(100)).max(1e3).optional(),
105
205
  /** Lifted from the transcript enrichment record; absent until transcription completes. */
106
206
  transcript: TimedTranscriptSchema.optional(),
107
207
  /**
108
- * Per-stage audio-processing status (transcribe / denoise), when the app
109
- * opted into processing on create. Absent otherwise. A `pending` stage
110
- * means the client should poll (or re-render) for the result. When
111
- * `denoise === 'ready'`, `url` above already resolves to the cleaned
112
- * audio variant. See `types/processing.ts`.
208
+ * Per-stage audio-processing status (denoise / trim / transcribe /
209
+ * waveform), when the app opted into processing. Absent otherwise. A
210
+ * `pending` stage means the client should poll (or re-render) for the
211
+ * result including a stage that returns to `pending` after having been
212
+ * `ready`, which is how a recompute surfaces. Once a byte-mutating stage
213
+ * completes, `url`/`durationMs`/`waveform` above already resolve to the
214
+ * processed audio variant. See `types/processing.ts`.
113
215
  */
114
216
  processing: ProcessingViewSchema.optional()
115
217
  });
@@ -213,10 +315,15 @@ var ViewerStateSchema = zod.z.object({
213
315
  replyDisabledReason: zod.z.enum(["unauthenticated", "not_a_participant"]).optional()
214
316
  });
215
317
  var PostRecordPublicSchema = zod.z.object({
216
- text: zod.z.string(),
217
- title: zod.z.string().optional(),
318
+ // `text`, `title` and `langs` keep the record's bounds, for the same reason
319
+ // `AudioEmbedViewSchema.alt` does: a view must never be able to carry a
320
+ // larger payload than the record it projects. The write path already
321
+ // enforces these, so no stored post can exceed them — stating them here
322
+ // keeps the published contract honest rather than adding a new constraint.
323
+ text: zod.z.string().max(3e3),
324
+ title: zod.z.string().max(3e3).optional(),
218
325
  reply: ReplyRefSchema.optional(),
219
- langs: zod.z.array(zod.z.string()).optional(),
326
+ langs: zod.z.array(zod.z.string()).max(3).optional(),
220
327
  selfLabels: zod.z.array(zod.z.string()).optional(),
221
328
  createdAt: FirestoreTimestampSchema
222
329
  });