mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 23:56:36 +08:00
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:
@@ -1,5 +1,5 @@
|
||||
mutation submitAudioTranscription($workspaceId: String!, $blobId: String!, $blob: Upload, $blobs: [Upload!]) {
|
||||
submitAudioTranscription(blob: $blob, blobs: $blobs, blobId: $blobId, workspaceId: $workspaceId) {
|
||||
mutation submitAudioTranscription($workspaceId: String!, $blobId: String!, $blob: Upload, $blobs: [Upload!], $input: SubmitAudioTranscriptionInput) {
|
||||
submitAudioTranscription(blob: $blob, blobs: $blobs, blobId: $blobId, workspaceId: $workspaceId, input: $input) {
|
||||
id
|
||||
status
|
||||
}
|
||||
|
||||
@@ -5,6 +5,48 @@ mutation claimAudioTranscription($jobId: String!) {
|
||||
title
|
||||
summary
|
||||
actions
|
||||
sourceAudio {
|
||||
blobId
|
||||
mimeType
|
||||
durationMs
|
||||
sampleRate
|
||||
channels
|
||||
}
|
||||
quality {
|
||||
degraded
|
||||
overflowCount
|
||||
}
|
||||
sliceManifest {
|
||||
index
|
||||
fileName
|
||||
mimeType
|
||||
startSec
|
||||
durationSec
|
||||
byteSize
|
||||
}
|
||||
normalizedSegments {
|
||||
speaker
|
||||
startSec
|
||||
endSec
|
||||
start
|
||||
end
|
||||
text
|
||||
}
|
||||
normalizedTranscript
|
||||
summaryJson {
|
||||
title
|
||||
durationMinutes
|
||||
attendees
|
||||
keyPoints
|
||||
actionItems {
|
||||
description
|
||||
owner
|
||||
deadline
|
||||
}
|
||||
decisions
|
||||
openQuestions
|
||||
blockers
|
||||
}
|
||||
transcription {
|
||||
speaker
|
||||
start
|
||||
|
||||
@@ -10,6 +10,48 @@ query getAudioTranscription(
|
||||
status
|
||||
title
|
||||
summary
|
||||
sourceAudio {
|
||||
blobId
|
||||
mimeType
|
||||
durationMs
|
||||
sampleRate
|
||||
channels
|
||||
}
|
||||
quality {
|
||||
degraded
|
||||
overflowCount
|
||||
}
|
||||
sliceManifest {
|
||||
index
|
||||
fileName
|
||||
mimeType
|
||||
startSec
|
||||
durationSec
|
||||
byteSize
|
||||
}
|
||||
normalizedSegments {
|
||||
speaker
|
||||
startSec
|
||||
endSec
|
||||
start
|
||||
end
|
||||
text
|
||||
}
|
||||
normalizedTranscript
|
||||
summaryJson {
|
||||
title
|
||||
durationMinutes
|
||||
attendees
|
||||
keyPoints
|
||||
actionItems {
|
||||
description
|
||||
owner
|
||||
deadline
|
||||
}
|
||||
decisions
|
||||
openQuestions
|
||||
blockers
|
||||
}
|
||||
transcription {
|
||||
speaker
|
||||
start
|
||||
|
||||
@@ -1417,12 +1417,13 @@ ${paginatedCopilotChatsFragment}`,
|
||||
export const submitAudioTranscriptionMutation = {
|
||||
id: 'submitAudioTranscriptionMutation' as const,
|
||||
op: 'submitAudioTranscription',
|
||||
query: `mutation submitAudioTranscription($workspaceId: String!, $blobId: String!, $blob: Upload, $blobs: [Upload!]) {
|
||||
query: `mutation submitAudioTranscription($workspaceId: String!, $blobId: String!, $blob: Upload, $blobs: [Upload!], $input: SubmitAudioTranscriptionInput) {
|
||||
submitAudioTranscription(
|
||||
blob: $blob
|
||||
blobs: $blobs
|
||||
blobId: $blobId
|
||||
workspaceId: $workspaceId
|
||||
input: $input
|
||||
) {
|
||||
id
|
||||
status
|
||||
@@ -1441,6 +1442,48 @@ export const claimAudioTranscriptionMutation = {
|
||||
title
|
||||
summary
|
||||
actions
|
||||
sourceAudio {
|
||||
blobId
|
||||
mimeType
|
||||
durationMs
|
||||
sampleRate
|
||||
channels
|
||||
}
|
||||
quality {
|
||||
degraded
|
||||
overflowCount
|
||||
}
|
||||
sliceManifest {
|
||||
index
|
||||
fileName
|
||||
mimeType
|
||||
startSec
|
||||
durationSec
|
||||
byteSize
|
||||
}
|
||||
normalizedSegments {
|
||||
speaker
|
||||
startSec
|
||||
endSec
|
||||
start
|
||||
end
|
||||
text
|
||||
}
|
||||
normalizedTranscript
|
||||
summaryJson {
|
||||
title
|
||||
durationMinutes
|
||||
attendees
|
||||
keyPoints
|
||||
actionItems {
|
||||
description
|
||||
owner
|
||||
deadline
|
||||
}
|
||||
decisions
|
||||
openQuestions
|
||||
blockers
|
||||
}
|
||||
transcription {
|
||||
speaker
|
||||
start
|
||||
@@ -1462,6 +1505,48 @@ export const getAudioTranscriptionQuery = {
|
||||
status
|
||||
title
|
||||
summary
|
||||
sourceAudio {
|
||||
blobId
|
||||
mimeType
|
||||
durationMs
|
||||
sampleRate
|
||||
channels
|
||||
}
|
||||
quality {
|
||||
degraded
|
||||
overflowCount
|
||||
}
|
||||
sliceManifest {
|
||||
index
|
||||
fileName
|
||||
mimeType
|
||||
startSec
|
||||
durationSec
|
||||
byteSize
|
||||
}
|
||||
normalizedSegments {
|
||||
speaker
|
||||
startSec
|
||||
endSec
|
||||
start
|
||||
end
|
||||
text
|
||||
}
|
||||
normalizedTranscript
|
||||
summaryJson {
|
||||
title
|
||||
durationMinutes
|
||||
attendees
|
||||
keyPoints
|
||||
actionItems {
|
||||
description
|
||||
owner
|
||||
deadline
|
||||
}
|
||||
decisions
|
||||
openQuestions
|
||||
blockers
|
||||
}
|
||||
transcription {
|
||||
speaker
|
||||
start
|
||||
|
||||
@@ -284,6 +284,25 @@ export interface AppConfigValidateResult {
|
||||
value: Scalars['JSON']['output'];
|
||||
}
|
||||
|
||||
export interface AudioSliceManifestItemInput {
|
||||
byteSize?: InputMaybe<Scalars['Int']['input']>;
|
||||
durationSec: Scalars['Float']['input'];
|
||||
fileName: Scalars['String']['input'];
|
||||
index: Scalars['Int']['input'];
|
||||
mimeType: Scalars['String']['input'];
|
||||
startSec: Scalars['Float']['input'];
|
||||
}
|
||||
|
||||
export interface AudioSliceManifestItemType {
|
||||
__typename?: 'AudioSliceManifestItemType';
|
||||
byteSize: Maybe<Scalars['Int']['output']>;
|
||||
durationSec: Scalars['Float']['output'];
|
||||
fileName: Scalars['String']['output'];
|
||||
index: Scalars['Int']['output'];
|
||||
mimeType: Scalars['String']['output'];
|
||||
startSec: Scalars['Float']['output'];
|
||||
}
|
||||
|
||||
export interface BlobNotFoundDataType {
|
||||
__typename?: 'BlobNotFoundDataType';
|
||||
blobId: Scalars['String']['output'];
|
||||
@@ -1268,6 +1287,7 @@ export enum FeatureType {
|
||||
FreePlan = 'FreePlan',
|
||||
LifetimeProPlan = 'LifetimeProPlan',
|
||||
ProPlan = 'ProPlan',
|
||||
QuotaExceededReadonlyWorkspace = 'QuotaExceededReadonlyWorkspace',
|
||||
TeamPlan = 'TeamPlan',
|
||||
UnlimitedCopilot = 'UnlimitedCopilot',
|
||||
UnlimitedWorkspace = 'UnlimitedWorkspace',
|
||||
@@ -1618,6 +1638,25 @@ export interface ManageUserInput {
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
}
|
||||
|
||||
export interface MeetingActionItemType {
|
||||
__typename?: 'MeetingActionItemType';
|
||||
deadline: Maybe<Scalars['String']['output']>;
|
||||
description: Scalars['String']['output'];
|
||||
owner: Maybe<Scalars['String']['output']>;
|
||||
}
|
||||
|
||||
export interface MeetingSummaryV2Type {
|
||||
__typename?: 'MeetingSummaryV2Type';
|
||||
actionItems: Array<MeetingActionItemType>;
|
||||
attendees: Array<Scalars['String']['output']>;
|
||||
blockers: Array<Scalars['String']['output']>;
|
||||
decisions: Array<Scalars['String']['output']>;
|
||||
durationMinutes: Scalars['Float']['output'];
|
||||
keyPoints: Array<Scalars['String']['output']>;
|
||||
openQuestions: Array<Scalars['String']['output']>;
|
||||
title: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface MemberNotFoundInSpaceDataType {
|
||||
__typename?: 'MemberNotFoundInSpaceDataType';
|
||||
spaceId: Scalars['String']['output'];
|
||||
@@ -2199,6 +2238,7 @@ export interface MutationSubmitAudioTranscriptionArgs {
|
||||
blob?: InputMaybe<Scalars['Upload']['input']>;
|
||||
blobId: Scalars['String']['input'];
|
||||
blobs?: InputMaybe<Array<Scalars['Upload']['input']>>;
|
||||
input?: InputMaybe<SubmitAudioTranscriptionInput>;
|
||||
workspaceId: Scalars['String']['input'];
|
||||
}
|
||||
|
||||
@@ -2298,6 +2338,16 @@ export interface NoMoreSeatDataType {
|
||||
spaceId: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface NormalizedTranscriptSegmentType {
|
||||
__typename?: 'NormalizedTranscriptSegmentType';
|
||||
end: Scalars['String']['output'];
|
||||
endSec: Scalars['Float']['output'];
|
||||
speaker: Scalars['String']['output'];
|
||||
start: Scalars['String']['output'];
|
||||
startSec: Scalars['Float']['output'];
|
||||
text: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface NotInSpaceDataType {
|
||||
__typename?: 'NotInSpaceDataType';
|
||||
spaceId: Scalars['String']['output'];
|
||||
@@ -2906,6 +2956,12 @@ export interface StreamObject {
|
||||
type: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface SubmitAudioTranscriptionInput {
|
||||
quality?: InputMaybe<TranscriptionQualityInput>;
|
||||
sliceManifest?: InputMaybe<Array<AudioSliceManifestItemInput>>;
|
||||
sourceAudio?: InputMaybe<TranscriptionSourceAudioInput>;
|
||||
}
|
||||
|
||||
export interface SubscriptionAlreadyExistsDataType {
|
||||
__typename?: 'SubscriptionAlreadyExistsDataType';
|
||||
plan: Scalars['String']['output'];
|
||||
@@ -3013,16 +3069,49 @@ export interface TranscriptionItemType {
|
||||
transcription: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface TranscriptionQualityInput {
|
||||
degraded?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
overflowCount?: InputMaybe<Scalars['Int']['input']>;
|
||||
}
|
||||
|
||||
export interface TranscriptionQualityType {
|
||||
__typename?: 'TranscriptionQualityType';
|
||||
degraded: Maybe<Scalars['Boolean']['output']>;
|
||||
overflowCount: Maybe<Scalars['Int']['output']>;
|
||||
}
|
||||
|
||||
export interface TranscriptionResultType {
|
||||
__typename?: 'TranscriptionResultType';
|
||||
actions: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
normalizedSegments: Maybe<Array<NormalizedTranscriptSegmentType>>;
|
||||
normalizedTranscript: Maybe<Scalars['String']['output']>;
|
||||
quality: Maybe<TranscriptionQualityType>;
|
||||
sliceManifest: Maybe<Array<AudioSliceManifestItemType>>;
|
||||
sourceAudio: Maybe<TranscriptionSourceAudioType>;
|
||||
status: AiJobStatus;
|
||||
summary: Maybe<Scalars['String']['output']>;
|
||||
summaryJson: Maybe<MeetingSummaryV2Type>;
|
||||
title: Maybe<Scalars['String']['output']>;
|
||||
transcription: Maybe<Array<TranscriptionItemType>>;
|
||||
}
|
||||
|
||||
export interface TranscriptionSourceAudioInput {
|
||||
channels?: InputMaybe<Scalars['Int']['input']>;
|
||||
durationMs?: InputMaybe<Scalars['Int']['input']>;
|
||||
mimeType?: InputMaybe<Scalars['String']['input']>;
|
||||
sampleRate?: InputMaybe<Scalars['Int']['input']>;
|
||||
}
|
||||
|
||||
export interface TranscriptionSourceAudioType {
|
||||
__typename?: 'TranscriptionSourceAudioType';
|
||||
blobId: Maybe<Scalars['String']['output']>;
|
||||
channels: Maybe<Scalars['Int']['output']>;
|
||||
durationMs: Maybe<Scalars['Int']['output']>;
|
||||
mimeType: Maybe<Scalars['String']['output']>;
|
||||
sampleRate: Maybe<Scalars['Int']['output']>;
|
||||
}
|
||||
|
||||
export type UnionNotificationBodyType =
|
||||
| InvitationAcceptedNotificationBodyType
|
||||
| InvitationBlockedNotificationBodyType
|
||||
@@ -5181,6 +5270,7 @@ export type SubmitAudioTranscriptionMutationVariables = Exact<{
|
||||
blobs?: InputMaybe<
|
||||
Array<Scalars['Upload']['input']> | Scalars['Upload']['input']
|
||||
>;
|
||||
input?: InputMaybe<SubmitAudioTranscriptionInput>;
|
||||
}>;
|
||||
|
||||
export type SubmitAudioTranscriptionMutation = {
|
||||
@@ -5205,6 +5295,54 @@ export type ClaimAudioTranscriptionMutation = {
|
||||
title: string | null;
|
||||
summary: string | null;
|
||||
actions: string | null;
|
||||
normalizedTranscript: string | null;
|
||||
sourceAudio: {
|
||||
__typename?: 'TranscriptionSourceAudioType';
|
||||
blobId: string | null;
|
||||
mimeType: string | null;
|
||||
durationMs: number | null;
|
||||
sampleRate: number | null;
|
||||
channels: number | null;
|
||||
} | null;
|
||||
quality: {
|
||||
__typename?: 'TranscriptionQualityType';
|
||||
degraded: boolean | null;
|
||||
overflowCount: number | null;
|
||||
} | null;
|
||||
sliceManifest: Array<{
|
||||
__typename?: 'AudioSliceManifestItemType';
|
||||
index: number;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
startSec: number;
|
||||
durationSec: number;
|
||||
byteSize: number | null;
|
||||
}> | null;
|
||||
normalizedSegments: Array<{
|
||||
__typename?: 'NormalizedTranscriptSegmentType';
|
||||
speaker: string;
|
||||
startSec: number;
|
||||
endSec: number;
|
||||
start: string;
|
||||
end: string;
|
||||
text: string;
|
||||
}> | null;
|
||||
summaryJson: {
|
||||
__typename?: 'MeetingSummaryV2Type';
|
||||
title: string;
|
||||
durationMinutes: number;
|
||||
attendees: Array<string>;
|
||||
keyPoints: Array<string>;
|
||||
decisions: Array<string>;
|
||||
openQuestions: Array<string>;
|
||||
blockers: Array<string>;
|
||||
actionItems: Array<{
|
||||
__typename?: 'MeetingActionItemType';
|
||||
description: string;
|
||||
owner: string | null;
|
||||
deadline: string | null;
|
||||
}>;
|
||||
} | null;
|
||||
transcription: Array<{
|
||||
__typename?: 'TranscriptionItemType';
|
||||
speaker: string;
|
||||
@@ -5233,6 +5371,54 @@ export type GetAudioTranscriptionQuery = {
|
||||
status: AiJobStatus;
|
||||
title: string | null;
|
||||
summary: string | null;
|
||||
normalizedTranscript: string | null;
|
||||
sourceAudio: {
|
||||
__typename?: 'TranscriptionSourceAudioType';
|
||||
blobId: string | null;
|
||||
mimeType: string | null;
|
||||
durationMs: number | null;
|
||||
sampleRate: number | null;
|
||||
channels: number | null;
|
||||
} | null;
|
||||
quality: {
|
||||
__typename?: 'TranscriptionQualityType';
|
||||
degraded: boolean | null;
|
||||
overflowCount: number | null;
|
||||
} | null;
|
||||
sliceManifest: Array<{
|
||||
__typename?: 'AudioSliceManifestItemType';
|
||||
index: number;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
startSec: number;
|
||||
durationSec: number;
|
||||
byteSize: number | null;
|
||||
}> | null;
|
||||
normalizedSegments: Array<{
|
||||
__typename?: 'NormalizedTranscriptSegmentType';
|
||||
speaker: string;
|
||||
startSec: number;
|
||||
endSec: number;
|
||||
start: string;
|
||||
end: string;
|
||||
text: string;
|
||||
}> | null;
|
||||
summaryJson: {
|
||||
__typename?: 'MeetingSummaryV2Type';
|
||||
title: string;
|
||||
durationMinutes: number;
|
||||
attendees: Array<string>;
|
||||
keyPoints: Array<string>;
|
||||
decisions: Array<string>;
|
||||
openQuestions: Array<string>;
|
||||
blockers: Array<string>;
|
||||
actionItems: Array<{
|
||||
__typename?: 'MeetingActionItemType';
|
||||
description: string;
|
||||
owner: string | null;
|
||||
deadline: string | null;
|
||||
}>;
|
||||
} | null;
|
||||
transcription: Array<{
|
||||
__typename?: 'TranscriptionItemType';
|
||||
speaker: string;
|
||||
|
||||
Reference in New Issue
Block a user