feat(server): refactor record schema (#14729)

#### PR Dependency Tree


* **PR #14729** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Transcriptions now produce structured meeting summaries (strict JSON)
and a normalized, speaker‑tagged, non‑overlapping transcript with legacy
projection support.

* **API**
* Submission accepts richer transcription input; results return
source‑audio metadata, slice manifest, quality indicators, normalized
segments/transcript, and structured summary JSON.

* **Frontend**
* Recording flow stores transcription metadata and uploads preprocessed
audio slices with slice/quality info; UI-side result normalization
applied.

* **Tests**
* Expanded unit, contract, and e2e coverage for normalization, payload
parsing, persistence/retry, and end‑to‑end transcription flows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-03-26 21:32:36 +08:00
committed by GitHub
parent a3379c8979
commit 5b05c5a1b2
29 changed files with 2977 additions and 463 deletions
@@ -16,6 +16,21 @@ import { getCurrentWorkspace, isAiEnabled } from './utils';
const logger = new DebugLogger('electron-renderer:recording');
const RECORDING_IMPORT_RETRY_MS = 1000;
const NATIVE_RECORDING_MIME_TYPE = 'audio/ogg';
const TRANSCRIPTION_BLOCK_FLAVOUR = 'affine:transcription';
type TranscriptionBlockModel = {
props: {
transcription: {
sourceAudio?: {
mimeType?: string;
durationMs?: number;
sampleRate?: number;
channels?: number;
};
quality?: { degraded?: boolean; overflowCount?: number };
};
};
};
type RecordingImportStatus = {
id: number;
@@ -115,6 +130,29 @@ function findExistingAttachment(docStore: Store, attachmentName: string) {
);
}
function ensureTranscriptionBlock(model: AttachmentBlockModel) {
for (const key of model.childMap.value.keys()) {
const block = model.store.getBlock$(key);
if (block?.flavour === TRANSCRIPTION_BLOCK_FLAVOUR) {
return block.model as unknown as TranscriptionBlockModel;
}
}
const blockId = model.store.addBlock(
TRANSCRIPTION_BLOCK_FLAVOUR,
{ transcription: {} },
model.id
);
const block = model.store.getBlock(blockId)?.model as
| TranscriptionBlockModel
| undefined;
if (!block) {
throw new Error('Failed to create transcription block');
}
return block;
}
async function createRecordingDoc(
frameworkProvider: FrameworkProvider,
workspace: WorkspaceHandle['workspace'],
@@ -187,6 +225,20 @@ async function createRecordingDoc(
attachmentCreated = true;
}
const transcriptionBlock = ensureTranscriptionBlock(model);
transcriptionBlock.props.transcription = {
sourceAudio: {
mimeType: model.props.type,
durationMs: status.durationMs,
sampleRate: status.sampleRate,
channels: status.numberOfChannels,
},
quality: {
degraded: status.degraded,
overflowCount: status.overflowCount,
},
};
workspace.scope.get(WorkbenchService).workbench.openDoc(targetDocId);
if (!aiEnabled || !attachmentCreated) {