Compare commits

...

3 Commits

Author SHA1 Message Date
Dean Wenstrand
a3460842ac Add VideoPlayerClusters query + FinalizePlayerAssignments mutation
All checks were successful
Tests / Tests (pull_request) Successful in 10s
Operation file for the per-video player-labeling UI that consumes the
schema types added in #240.

  - VideoPlayerClusters($videoId): read clusters + crops
  - FinalizePlayerAssignments($input): apply user assignments and
    shot moves in one transaction

Generated by `just gql`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:44:42 -07:00
84d3a0252d Merge pull request 'Add player cluster + labeling mutation types' (#240) from dean/labeling-api-types into master
Reviewed-on: #240
2026-05-09 00:38:29 +00:00
Dean Wenstrand
1de4a97cb6 Add player cluster + labeling mutation types
All checks were successful
Tests / Tests (pull_request) Successful in 10s
Schema additions for the per-video player labeling UI:
  * `PlayerClusterGQL` / `PlayerClusterShotGQL` — read shape.
  * `FinalizePlayerAssignmentsInput` + `ClusterAssignmentInput` +
    `ShotMoveInput` — write shape.
  * Query: `videoPlayerClusters(videoId)`.
  * Mutation: `finalizePlayerAssignments(input)`.

Generated by `just gql` from the backend type definitions in
`railbird/datatypes/gql/shooter.py` and resolvers in
`railbird/server/resolvers/shooter.py`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:04:41 -07:00
3 changed files with 366 additions and 0 deletions

View File

@@ -205,6 +205,11 @@ export enum ClientUploadStatusEnum {
UploadEnabled = "UPLOAD_ENABLED",
}
export type ClusterAssignmentInput = {
clusterId: Scalars["Int"]["input"];
userId?: InputMaybe<Scalars["Int"]["input"]>;
};
export type CommentGql = {
__typename?: "CommentGQL";
id: Scalars["Int"]["output"];
@@ -2182,6 +2187,12 @@ export type FilterInput =
videoId: Array<Scalars["Int"]["input"]>;
};
export type FinalizePlayerAssignmentsInput = {
clusterAssignments?: Array<ClusterAssignmentInput>;
shotMoves?: Array<ShotMoveInput>;
videoId: Scalars["Int"]["input"];
};
export type FloatOrdering = {
descending?: Scalars["Boolean"]["input"];
startingAt?: InputMaybe<Scalars["Float"]["input"]>;
@@ -2394,6 +2405,7 @@ export type Mutation = {
editUploadStream: Scalars["Boolean"]["output"];
editUser: UserGql;
ensureStripeCustomerExists: UserGql;
finalizePlayerAssignments: Array<PlayerClusterGql>;
findPrerecordTableLayout?: Maybe<HomographyInfoGql>;
followUser: UserGql;
getHlsInitUploadLink: GetUploadLinkReturn;
@@ -2517,6 +2529,10 @@ export type MutationEditUserArgs = {
input: EditUserInputGql;
};
export type MutationFinalizePlayerAssignmentsArgs = {
input: FinalizePlayerAssignmentsInput;
};
export type MutationFindPrerecordTableLayoutArgs = {
b64Image: Scalars["String"]["input"];
videoId: Scalars["Int"]["input"];
@@ -2679,6 +2695,29 @@ export type PageInfoGql = {
hasNextPage: Scalars["Boolean"]["output"];
};
export type PlayerClusterGql = {
__typename?: "PlayerClusterGQL";
clusterId: Scalars["Int"]["output"];
confirmed: Scalars["Boolean"]["output"];
nShots: Scalars["Int"]["output"];
shots: Array<PlayerClusterShotGql>;
userId?: Maybe<Scalars["Int"]["output"]>;
videoId: Scalars["Int"]["output"];
};
export type PlayerClusterShotGql = {
__typename?: "PlayerClusterShotGQL";
bboxX1: Scalars["Int"]["output"];
bboxX2: Scalars["Int"]["output"];
bboxY1: Scalars["Int"]["output"];
bboxY2: Scalars["Int"]["output"];
confidence: Scalars["Float"]["output"];
cropUrl?: Maybe<Scalars["String"]["output"]>;
fullFrameUrl?: Maybe<Scalars["String"]["output"]>;
isConfirmed: Scalars["Boolean"]["output"];
shotId: Scalars["Int"]["output"];
};
export enum PocketEnum {
Corner = "CORNER",
Side = "SIDE",
@@ -2789,6 +2828,7 @@ export type Query = {
notifications: NotificationConnection;
ruleSets: Array<RuleSet>;
unreadNotificationCount: Scalars["Int"]["output"];
videoPlayerClusters: Array<PlayerClusterGql>;
waitFor: Scalars["Float"]["output"];
};
@@ -2955,6 +2995,10 @@ export type QueryNotificationsArgs = {
offset?: Scalars["Int"]["input"];
};
export type QueryVideoPlayerClustersArgs = {
videoId: Scalars["Int"]["input"];
};
export type QueryWaitForArgs = {
duration: Scalars["Float"]["input"];
};
@@ -3172,6 +3216,11 @@ export type ShotGql = {
videoId: Scalars["Int"]["output"];
};
export type ShotMoveInput = {
newClusterId: Scalars["Int"]["input"];
shotId: Scalars["Int"]["input"];
};
export type ShotsOrderingComponent =
| {
difficulty: FloatOrdering;
@@ -4880,6 +4929,96 @@ export type GetRunsWithTimestampsQuery = {
};
};
export type PlayerClusterShotFieldsFragment = {
__typename?: "PlayerClusterShotGQL";
shotId: number;
bboxX1: number;
bboxY1: number;
bboxX2: number;
bboxY2: number;
confidence: number;
isConfirmed: boolean;
cropUrl?: string | null;
fullFrameUrl?: string | null;
};
export type PlayerClusterFieldsFragment = {
__typename?: "PlayerClusterGQL";
videoId: number;
clusterId: number;
nShots: number;
userId?: number | null;
confirmed: boolean;
shots: Array<{
__typename?: "PlayerClusterShotGQL";
shotId: number;
bboxX1: number;
bboxY1: number;
bboxX2: number;
bboxY2: number;
confidence: number;
isConfirmed: boolean;
cropUrl?: string | null;
fullFrameUrl?: string | null;
}>;
};
export type VideoPlayerClustersQueryVariables = Exact<{
videoId: Scalars["Int"]["input"];
}>;
export type VideoPlayerClustersQuery = {
__typename?: "Query";
videoPlayerClusters: Array<{
__typename?: "PlayerClusterGQL";
videoId: number;
clusterId: number;
nShots: number;
userId?: number | null;
confirmed: boolean;
shots: Array<{
__typename?: "PlayerClusterShotGQL";
shotId: number;
bboxX1: number;
bboxY1: number;
bboxX2: number;
bboxY2: number;
confidence: number;
isConfirmed: boolean;
cropUrl?: string | null;
fullFrameUrl?: string | null;
}>;
}>;
};
export type FinalizePlayerAssignmentsMutationVariables = Exact<{
input: FinalizePlayerAssignmentsInput;
}>;
export type FinalizePlayerAssignmentsMutation = {
__typename?: "Mutation";
finalizePlayerAssignments: Array<{
__typename?: "PlayerClusterGQL";
videoId: number;
clusterId: number;
nShots: number;
userId?: number | null;
confirmed: boolean;
shots: Array<{
__typename?: "PlayerClusterShotGQL";
shotId: number;
bboxX1: number;
bboxY1: number;
bboxX2: number;
bboxY2: number;
confidence: number;
isConfirmed: boolean;
cropUrl?: string | null;
fullFrameUrl?: string | null;
}>;
}>;
};
export type GetSerializedShotPathsQueryVariables = Exact<{
filterInput: FilterInput;
}>;
@@ -6583,6 +6722,32 @@ export const PocketingIntentionFragmentFragmentDoc = gql`
difficulty
}
`;
export const PlayerClusterShotFieldsFragmentDoc = gql`
fragment PlayerClusterShotFields on PlayerClusterShotGQL {
shotId
bboxX1
bboxY1
bboxX2
bboxY2
confidence
isConfirmed
cropUrl
fullFrameUrl
}
`;
export const PlayerClusterFieldsFragmentDoc = gql`
fragment PlayerClusterFields on PlayerClusterGQL {
videoId
clusterId
nShots
userId
confirmed
shots {
...PlayerClusterShotFields
}
}
${PlayerClusterShotFieldsFragmentDoc}
`;
export const ShotWithAllFeaturesFragmentDoc = gql`
fragment ShotWithAllFeatures on ShotGQL {
id
@@ -10186,6 +10351,132 @@ export type GetRunsWithTimestampsQueryResult = Apollo.QueryResult<
GetRunsWithTimestampsQuery,
GetRunsWithTimestampsQueryVariables
>;
export const VideoPlayerClustersDocument = gql`
query VideoPlayerClusters($videoId: Int!) {
videoPlayerClusters(videoId: $videoId) {
...PlayerClusterFields
}
}
${PlayerClusterFieldsFragmentDoc}
`;
/**
* __useVideoPlayerClustersQuery__
*
* To run a query within a React component, call `useVideoPlayerClustersQuery` and pass it any options that fit your needs.
* When your component renders, `useVideoPlayerClustersQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useVideoPlayerClustersQuery({
* variables: {
* videoId: // value for 'videoId'
* },
* });
*/
export function useVideoPlayerClustersQuery(
baseOptions: Apollo.QueryHookOptions<
VideoPlayerClustersQuery,
VideoPlayerClustersQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
VideoPlayerClustersQuery,
VideoPlayerClustersQueryVariables
>(VideoPlayerClustersDocument, options);
}
export function useVideoPlayerClustersLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
VideoPlayerClustersQuery,
VideoPlayerClustersQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
VideoPlayerClustersQuery,
VideoPlayerClustersQueryVariables
>(VideoPlayerClustersDocument, options);
}
export function useVideoPlayerClustersSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
VideoPlayerClustersQuery,
VideoPlayerClustersQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
VideoPlayerClustersQuery,
VideoPlayerClustersQueryVariables
>(VideoPlayerClustersDocument, options);
}
export type VideoPlayerClustersQueryHookResult = ReturnType<
typeof useVideoPlayerClustersQuery
>;
export type VideoPlayerClustersLazyQueryHookResult = ReturnType<
typeof useVideoPlayerClustersLazyQuery
>;
export type VideoPlayerClustersSuspenseQueryHookResult = ReturnType<
typeof useVideoPlayerClustersSuspenseQuery
>;
export type VideoPlayerClustersQueryResult = Apollo.QueryResult<
VideoPlayerClustersQuery,
VideoPlayerClustersQueryVariables
>;
export const FinalizePlayerAssignmentsDocument = gql`
mutation FinalizePlayerAssignments($input: FinalizePlayerAssignmentsInput!) {
finalizePlayerAssignments(input: $input) {
...PlayerClusterFields
}
}
${PlayerClusterFieldsFragmentDoc}
`;
export type FinalizePlayerAssignmentsMutationFn = Apollo.MutationFunction<
FinalizePlayerAssignmentsMutation,
FinalizePlayerAssignmentsMutationVariables
>;
/**
* __useFinalizePlayerAssignmentsMutation__
*
* To run a mutation, you first call `useFinalizePlayerAssignmentsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useFinalizePlayerAssignmentsMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [finalizePlayerAssignmentsMutation, { data, loading, error }] = useFinalizePlayerAssignmentsMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useFinalizePlayerAssignmentsMutation(
baseOptions?: Apollo.MutationHookOptions<
FinalizePlayerAssignmentsMutation,
FinalizePlayerAssignmentsMutationVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<
FinalizePlayerAssignmentsMutation,
FinalizePlayerAssignmentsMutationVariables
>(FinalizePlayerAssignmentsDocument, options);
}
export type FinalizePlayerAssignmentsMutationHookResult = ReturnType<
typeof useFinalizePlayerAssignmentsMutation
>;
export type FinalizePlayerAssignmentsMutationResult =
Apollo.MutationResult<FinalizePlayerAssignmentsMutation>;
export type FinalizePlayerAssignmentsMutationOptions =
Apollo.BaseMutationOptions<
FinalizePlayerAssignmentsMutation,
FinalizePlayerAssignmentsMutationVariables
>;
export const GetSerializedShotPathsDocument = gql`
query GetSerializedShotPaths($filterInput: FilterInput!) {
getShots(filterInput: $filterInput) {

View File

@@ -0,0 +1,34 @@
fragment PlayerClusterShotFields on PlayerClusterShotGQL {
shotId
bboxX1
bboxY1
bboxX2
bboxY2
confidence
isConfirmed
cropUrl
fullFrameUrl
}
fragment PlayerClusterFields on PlayerClusterGQL {
videoId
clusterId
nShots
userId
confirmed
shots {
...PlayerClusterShotFields
}
}
query VideoPlayerClusters($videoId: Int!) {
videoPlayerClusters(videoId: $videoId) {
...PlayerClusterFields
}
}
mutation FinalizePlayerAssignments($input: FinalizePlayerAssignmentsInput!) {
finalizePlayerAssignments(input: $input) {
...PlayerClusterFields
}
}

View File

@@ -49,6 +49,7 @@ type Query {
limit: Int! = 500
countRespectsLimit: Boolean! = false
): GetRunsResult!
videoPlayerClusters(videoId: Int!): [PlayerClusterGQL!]!
getShotAnnotationTypes(errorTypes: Boolean = false): [ShotAnnotationTypeGQL!]!
getTableState(
b64Image: String!
@@ -860,6 +861,27 @@ input DatetimeOrdering {
startingAt: DateTime = null
}
type PlayerClusterGQL {
videoId: Int!
clusterId: Int!
nShots: Int!
userId: Int
confirmed: Boolean!
shots: [PlayerClusterShotGQL!]!
}
type PlayerClusterShotGQL {
shotId: Int!
bboxX1: Int!
bboxY1: Int!
bboxX2: Int!
bboxY2: Int!
confidence: Float!
isConfirmed: Boolean!
cropUrl: String
fullFrameUrl: String
}
type TableStateGQL {
identifierToPosition: [[Float!]!]!
homography: HomographyInfoGQL
@@ -1094,6 +1116,9 @@ type Mutation {
markAllNotificationsAsRead: Boolean!
markNotificationsAsRead(notificationIds: [Int!]!): Boolean!
deleteNotification(notificationId: Int!): Boolean!
finalizePlayerAssignments(
input: FinalizePlayerAssignmentsInput!
): [PlayerClusterGQL!]!
addAnnotationToShot(
shotId: Int!
annotationName: String!
@@ -1165,6 +1190,22 @@ enum ReportReasonEnum {
OTHER
}
input FinalizePlayerAssignmentsInput {
videoId: Int!
clusterAssignments: [ClusterAssignmentInput!]! = []
shotMoves: [ShotMoveInput!]! = []
}
input ClusterAssignmentInput {
clusterId: Int!
userId: Int = null
}
input ShotMoveInput {
shotId: Int!
newClusterId: Int!
}
type AddShotAnnotationReturn {
value: SuccessfulAddAddShotAnnotationErrors!
}