diff --git a/api/v1/api.proto b/api/v1/api.proto index 5b686e1..29203f9 100644 --- a/api/v1/api.proto +++ b/api/v1/api.proto @@ -398,6 +398,109 @@ message GetVaultItemResponse { VaultItem item = 1; } +// ---- Markers ---- + +enum MarkerType { + MARKER_TYPE_UNSPECIFIED = 0; + MARKER_TYPE_NAME_SUPER = 1; + MARKER_TYPE_BIBLE_VERSE = 2; + MARKER_TYPE_SONG = 3; + MARKER_TYPE_CHAPTER = 4; + MARKER_TYPE_CUSTOM = 5; +} + +// Where a marker originated. IMPORTED markers come from the third-party timing +// program (name-supers, bible-verse references, …); MANUAL ones are created in +// this tool. The source is preserved through edits for reconciliation. +enum MarkerSource { + MARKER_SOURCE_UNSPECIFIED = 0; + MARKER_SOURCE_IMPORTED = 1; + MARKER_SOURCE_MANUAL = 2; +} + +message Marker { + string id = 1; + MarkerType type = 2; + // Display text: the name, the verse reference, the song/chapter title. + // Always present; for linked markers it mirrors the canonical entity label, + // for custom / free-text markers it's whatever the user typed. + string label = 3; + string note = 4; + // In/out points in seconds. + double start = 5; + double end = 6; + MarkerSource source = 7; + // Canonical entity reference, when the label is linked to a known entity (a + // bible passage, a song, a person). Empty for free-text / custom markers. + // `label` is kept as denormalized display text so linking is enrichment, not + // a requirement — unmatched markers still round-trip. + string entity_id = 8; + // Which registry `entity_id` belongs to: "bible" | "songbook" | "people". + // Empty when `entity_id` is empty. + string entity_source = 9; +} + +// A resolvable entity behind a marker label, returned by SearchEntities for +// autocomplete. `id`/`source` map to Marker.entity_id/entity_source. +message Entity { + string id = 1; + string source = 2; + // Canonical display text, e.g. "John 3:16". + string label = 3; + // Optional secondary text shown under the label, e.g. a translation or book. + string detail = 4; +} + +message SearchEntitiesRequest { + // Which kind of entity to search — mapped from the marker's type. + MarkerType type = 1; + string query = 2; + // Max results to return; server clamps to a sane bound. 0 means default. + int32 limit = 3; +} + +message SearchEntitiesResponse { + repeated Entity entities = 1; +} + +// One free-text label to try to resolve to a canonical entity, used by the bulk +// "resolve references" action. +message ReferenceQuery { + // Opaque id echoed back so the caller can correlate results (the marker id). + string ref_id = 1; + MarkerType type = 2; + string text = 3; +} + +message ResolvedReference { + string ref_id = 1; + // True only when the text resolved to a single, confident match. + bool resolved = 2; + // Set only when resolved. + Entity entity = 3; +} + +message ResolveReferencesRequest { + repeated ReferenceQuery queries = 1; +} + +message ResolveReferencesResponse { + repeated ResolvedReference results = 1; +} + +message GetMarkersRequest { + string VXID = 1; +} + +message GetMarkersResponse { + repeated Marker markers = 1; +} + +message SubmitMarkersRequest { + string VXID = 1; + repeated Marker markers = 2; +} + service APIService { // Permissions rpc GetPermissions (Void) returns (Permissions) {} @@ -441,4 +544,12 @@ service APIService { // VAULT (Vidispine search) rpc VaultSearch(VaultSearchRequest) returns (VaultSearchResponse) {} rpc GetVaultItem(GetVaultItemRequest) returns (GetVaultItemResponse) {} + + // Markers + rpc GetMarkers(GetMarkersRequest) returns (GetMarkersResponse) {} + rpc SubmitMarkers(SubmitMarkersRequest) returns (Void) {} + // Autocomplete for marker labels (bible passages, songs, people). + rpc SearchEntities(SearchEntitiesRequest) returns (SearchEntitiesResponse) {} + // Bulk-resolve free-text marker labels to canonical entities. + rpc ResolveReferences(ResolveReferencesRequest) returns (ResolveReferencesResponse) {} } diff --git a/backend/api/v1/api.pb.go b/backend/api/v1/api.pb.go index 2182bcc..804f2f1 100644 --- a/backend/api/v1/api.pb.go +++ b/backend/api/v1/api.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.8 // protoc (unknown) // source: api/v1/api.proto @@ -123,6 +123,116 @@ func (CantemoAction) EnumDescriptor() ([]byte, []int) { return file_api_v1_api_proto_rawDescGZIP(), []int{1} } +type MarkerType int32 + +const ( + MarkerType_MARKER_TYPE_UNSPECIFIED MarkerType = 0 + MarkerType_MARKER_TYPE_NAME_SUPER MarkerType = 1 + MarkerType_MARKER_TYPE_BIBLE_VERSE MarkerType = 2 + MarkerType_MARKER_TYPE_SONG MarkerType = 3 + MarkerType_MARKER_TYPE_CHAPTER MarkerType = 4 + MarkerType_MARKER_TYPE_CUSTOM MarkerType = 5 +) + +// Enum value maps for MarkerType. +var ( + MarkerType_name = map[int32]string{ + 0: "MARKER_TYPE_UNSPECIFIED", + 1: "MARKER_TYPE_NAME_SUPER", + 2: "MARKER_TYPE_BIBLE_VERSE", + 3: "MARKER_TYPE_SONG", + 4: "MARKER_TYPE_CHAPTER", + 5: "MARKER_TYPE_CUSTOM", + } + MarkerType_value = map[string]int32{ + "MARKER_TYPE_UNSPECIFIED": 0, + "MARKER_TYPE_NAME_SUPER": 1, + "MARKER_TYPE_BIBLE_VERSE": 2, + "MARKER_TYPE_SONG": 3, + "MARKER_TYPE_CHAPTER": 4, + "MARKER_TYPE_CUSTOM": 5, + } +) + +func (x MarkerType) Enum() *MarkerType { + p := new(MarkerType) + *p = x + return p +} + +func (x MarkerType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MarkerType) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_api_proto_enumTypes[2].Descriptor() +} + +func (MarkerType) Type() protoreflect.EnumType { + return &file_api_v1_api_proto_enumTypes[2] +} + +func (x MarkerType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MarkerType.Descriptor instead. +func (MarkerType) EnumDescriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{2} +} + +// Where a marker originated. IMPORTED markers come from the third-party timing +// program (name-supers, bible-verse references, …); MANUAL ones are created in +// this tool. The source is preserved through edits for reconciliation. +type MarkerSource int32 + +const ( + MarkerSource_MARKER_SOURCE_UNSPECIFIED MarkerSource = 0 + MarkerSource_MARKER_SOURCE_IMPORTED MarkerSource = 1 + MarkerSource_MARKER_SOURCE_MANUAL MarkerSource = 2 +) + +// Enum value maps for MarkerSource. +var ( + MarkerSource_name = map[int32]string{ + 0: "MARKER_SOURCE_UNSPECIFIED", + 1: "MARKER_SOURCE_IMPORTED", + 2: "MARKER_SOURCE_MANUAL", + } + MarkerSource_value = map[string]int32{ + "MARKER_SOURCE_UNSPECIFIED": 0, + "MARKER_SOURCE_IMPORTED": 1, + "MARKER_SOURCE_MANUAL": 2, + } +) + +func (x MarkerSource) Enum() *MarkerSource { + p := new(MarkerSource) + *p = x + return p +} + +func (x MarkerSource) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MarkerSource) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_api_proto_enumTypes[3].Descriptor() +} + +func (MarkerSource) Type() protoreflect.EnumType { + return &file_api_v1_api_proto_enumTypes[3] +} + +func (x MarkerSource) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MarkerSource.Descriptor instead. +func (MarkerSource) EnumDescriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{3} +} + type BMMPermission struct { state protoimpl.MessageState `protogen:"open.v1"` Languages []string `protobuf:"bytes,1,rep,name=languages,proto3" json:"languages,omitempty"` @@ -3502,6 +3612,655 @@ func (x *GetVaultItemResponse) GetItem() *VaultItem { return nil } +type Marker struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Type MarkerType `protobuf:"varint,2,opt,name=type,proto3,enum=api.v1.MarkerType" json:"type,omitempty"` + // Display text: the name, the verse reference, the song/chapter title. + // Always present; for linked markers it mirrors the canonical entity label, + // for custom / free-text markers it's whatever the user typed. + Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` + Note string `protobuf:"bytes,4,opt,name=note,proto3" json:"note,omitempty"` + // In/out points in seconds. + Start float64 `protobuf:"fixed64,5,opt,name=start,proto3" json:"start,omitempty"` + End float64 `protobuf:"fixed64,6,opt,name=end,proto3" json:"end,omitempty"` + Source MarkerSource `protobuf:"varint,7,opt,name=source,proto3,enum=api.v1.MarkerSource" json:"source,omitempty"` + // Canonical entity reference, when the label is linked to a known entity (a + // bible passage, a song, a person). Empty for free-text / custom markers. + // `label` is kept as denormalized display text so linking is enrichment, not + // a requirement — unmatched markers still round-trip. + EntityId string `protobuf:"bytes,8,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + // Which registry `entity_id` belongs to: "bible" | "songbook" | "people". + // Empty when `entity_id` is empty. + EntitySource string `protobuf:"bytes,9,opt,name=entity_source,json=entitySource,proto3" json:"entity_source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Marker) Reset() { + *x = Marker{} + mi := &file_api_v1_api_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Marker) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Marker) ProtoMessage() {} + +func (x *Marker) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Marker.ProtoReflect.Descriptor instead. +func (*Marker) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{57} +} + +func (x *Marker) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Marker) GetType() MarkerType { + if x != nil { + return x.Type + } + return MarkerType_MARKER_TYPE_UNSPECIFIED +} + +func (x *Marker) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *Marker) GetNote() string { + if x != nil { + return x.Note + } + return "" +} + +func (x *Marker) GetStart() float64 { + if x != nil { + return x.Start + } + return 0 +} + +func (x *Marker) GetEnd() float64 { + if x != nil { + return x.End + } + return 0 +} + +func (x *Marker) GetSource() MarkerSource { + if x != nil { + return x.Source + } + return MarkerSource_MARKER_SOURCE_UNSPECIFIED +} + +func (x *Marker) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + +func (x *Marker) GetEntitySource() string { + if x != nil { + return x.EntitySource + } + return "" +} + +// A resolvable entity behind a marker label, returned by SearchEntities for +// autocomplete. `id`/`source` map to Marker.entity_id/entity_source. +type Entity struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + // Canonical display text, e.g. "John 3:16". + Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` + // Optional secondary text shown under the label, e.g. a translation or book. + Detail string `protobuf:"bytes,4,opt,name=detail,proto3" json:"detail,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Entity) Reset() { + *x = Entity{} + mi := &file_api_v1_api_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Entity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Entity) ProtoMessage() {} + +func (x *Entity) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[58] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Entity.ProtoReflect.Descriptor instead. +func (*Entity) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{58} +} + +func (x *Entity) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Entity) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *Entity) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *Entity) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +type SearchEntitiesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Which kind of entity to search — mapped from the marker's type. + Type MarkerType `protobuf:"varint,1,opt,name=type,proto3,enum=api.v1.MarkerType" json:"type,omitempty"` + Query string `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` + // Max results to return; server clamps to a sane bound. 0 means default. + Limit int32 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchEntitiesRequest) Reset() { + *x = SearchEntitiesRequest{} + mi := &file_api_v1_api_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchEntitiesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchEntitiesRequest) ProtoMessage() {} + +func (x *SearchEntitiesRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[59] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchEntitiesRequest.ProtoReflect.Descriptor instead. +func (*SearchEntitiesRequest) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{59} +} + +func (x *SearchEntitiesRequest) GetType() MarkerType { + if x != nil { + return x.Type + } + return MarkerType_MARKER_TYPE_UNSPECIFIED +} + +func (x *SearchEntitiesRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *SearchEntitiesRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +type SearchEntitiesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entities []*Entity `protobuf:"bytes,1,rep,name=entities,proto3" json:"entities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchEntitiesResponse) Reset() { + *x = SearchEntitiesResponse{} + mi := &file_api_v1_api_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchEntitiesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchEntitiesResponse) ProtoMessage() {} + +func (x *SearchEntitiesResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[60] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchEntitiesResponse.ProtoReflect.Descriptor instead. +func (*SearchEntitiesResponse) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{60} +} + +func (x *SearchEntitiesResponse) GetEntities() []*Entity { + if x != nil { + return x.Entities + } + return nil +} + +// One free-text label to try to resolve to a canonical entity, used by the bulk +// "resolve references" action. +type ReferenceQuery struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Opaque id echoed back so the caller can correlate results (the marker id). + RefId string `protobuf:"bytes,1,opt,name=ref_id,json=refId,proto3" json:"ref_id,omitempty"` + Type MarkerType `protobuf:"varint,2,opt,name=type,proto3,enum=api.v1.MarkerType" json:"type,omitempty"` + Text string `protobuf:"bytes,3,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReferenceQuery) Reset() { + *x = ReferenceQuery{} + mi := &file_api_v1_api_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReferenceQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReferenceQuery) ProtoMessage() {} + +func (x *ReferenceQuery) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReferenceQuery.ProtoReflect.Descriptor instead. +func (*ReferenceQuery) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{61} +} + +func (x *ReferenceQuery) GetRefId() string { + if x != nil { + return x.RefId + } + return "" +} + +func (x *ReferenceQuery) GetType() MarkerType { + if x != nil { + return x.Type + } + return MarkerType_MARKER_TYPE_UNSPECIFIED +} + +func (x *ReferenceQuery) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +type ResolvedReference struct { + state protoimpl.MessageState `protogen:"open.v1"` + RefId string `protobuf:"bytes,1,opt,name=ref_id,json=refId,proto3" json:"ref_id,omitempty"` + // True only when the text resolved to a single, confident match. + Resolved bool `protobuf:"varint,2,opt,name=resolved,proto3" json:"resolved,omitempty"` + // Set only when resolved. + Entity *Entity `protobuf:"bytes,3,opt,name=entity,proto3" json:"entity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolvedReference) Reset() { + *x = ResolvedReference{} + mi := &file_api_v1_api_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolvedReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolvedReference) ProtoMessage() {} + +func (x *ResolvedReference) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[62] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolvedReference.ProtoReflect.Descriptor instead. +func (*ResolvedReference) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{62} +} + +func (x *ResolvedReference) GetRefId() string { + if x != nil { + return x.RefId + } + return "" +} + +func (x *ResolvedReference) GetResolved() bool { + if x != nil { + return x.Resolved + } + return false +} + +func (x *ResolvedReference) GetEntity() *Entity { + if x != nil { + return x.Entity + } + return nil +} + +type ResolveReferencesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Queries []*ReferenceQuery `protobuf:"bytes,1,rep,name=queries,proto3" json:"queries,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolveReferencesRequest) Reset() { + *x = ResolveReferencesRequest{} + mi := &file_api_v1_api_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolveReferencesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolveReferencesRequest) ProtoMessage() {} + +func (x *ResolveReferencesRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolveReferencesRequest.ProtoReflect.Descriptor instead. +func (*ResolveReferencesRequest) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{63} +} + +func (x *ResolveReferencesRequest) GetQueries() []*ReferenceQuery { + if x != nil { + return x.Queries + } + return nil +} + +type ResolveReferencesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Results []*ResolvedReference `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolveReferencesResponse) Reset() { + *x = ResolveReferencesResponse{} + mi := &file_api_v1_api_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolveReferencesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolveReferencesResponse) ProtoMessage() {} + +func (x *ResolveReferencesResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolveReferencesResponse.ProtoReflect.Descriptor instead. +func (*ResolveReferencesResponse) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{64} +} + +func (x *ResolveReferencesResponse) GetResults() []*ResolvedReference { + if x != nil { + return x.Results + } + return nil +} + +type GetMarkersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + VXID string `protobuf:"bytes,1,opt,name=VXID,proto3" json:"VXID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMarkersRequest) Reset() { + *x = GetMarkersRequest{} + mi := &file_api_v1_api_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMarkersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMarkersRequest) ProtoMessage() {} + +func (x *GetMarkersRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMarkersRequest.ProtoReflect.Descriptor instead. +func (*GetMarkersRequest) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{65} +} + +func (x *GetMarkersRequest) GetVXID() string { + if x != nil { + return x.VXID + } + return "" +} + +type GetMarkersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Markers []*Marker `protobuf:"bytes,1,rep,name=markers,proto3" json:"markers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMarkersResponse) Reset() { + *x = GetMarkersResponse{} + mi := &file_api_v1_api_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMarkersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMarkersResponse) ProtoMessage() {} + +func (x *GetMarkersResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMarkersResponse.ProtoReflect.Descriptor instead. +func (*GetMarkersResponse) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{66} +} + +func (x *GetMarkersResponse) GetMarkers() []*Marker { + if x != nil { + return x.Markers + } + return nil +} + +type SubmitMarkersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + VXID string `protobuf:"bytes,1,opt,name=VXID,proto3" json:"VXID,omitempty"` + Markers []*Marker `protobuf:"bytes,2,rep,name=markers,proto3" json:"markers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitMarkersRequest) Reset() { + *x = SubmitMarkersRequest{} + mi := &file_api_v1_api_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitMarkersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitMarkersRequest) ProtoMessage() {} + +func (x *SubmitMarkersRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_api_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmitMarkersRequest.ProtoReflect.Descriptor instead. +func (*SubmitMarkersRequest) Descriptor() ([]byte, []int) { + return file_api_v1_api_proto_rawDescGZIP(), []int{67} +} + +func (x *SubmitMarkersRequest) GetVXID() string { + if x != nil { + return x.VXID + } + return "" +} + +func (x *SubmitMarkersRequest) GetMarkers() []*Marker { + if x != nil { + return x.Markers + } + return nil +} + var File_api_v1_api_proto protoreflect.FileDescriptor const file_api_v1_api_proto_rawDesc = "" + @@ -3754,7 +4513,47 @@ const file_api_v1_api_proto_rawDesc = "" + "\x13GetVaultItemRequest\x12\x12\n" + "\x04VXID\x18\x01 \x01(\tR\x04VXID\"=\n" + "\x14GetVaultItemResponse\x12%\n" + - "\x04item\x18\x01 \x01(\v2\x11.api.v1.VaultItemR\x04item*1\n" + + "\x04item\x18\x01 \x01(\v2\x11.api.v1.VaultItemR\x04item\"\x82\x02\n" + + "\x06Marker\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12&\n" + + "\x04type\x18\x02 \x01(\x0e2\x12.api.v1.MarkerTypeR\x04type\x12\x14\n" + + "\x05label\x18\x03 \x01(\tR\x05label\x12\x12\n" + + "\x04note\x18\x04 \x01(\tR\x04note\x12\x14\n" + + "\x05start\x18\x05 \x01(\x01R\x05start\x12\x10\n" + + "\x03end\x18\x06 \x01(\x01R\x03end\x12,\n" + + "\x06source\x18\a \x01(\x0e2\x14.api.v1.MarkerSourceR\x06source\x12\x1b\n" + + "\tentity_id\x18\b \x01(\tR\bentityId\x12#\n" + + "\rentity_source\x18\t \x01(\tR\fentitySource\"^\n" + + "\x06Entity\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + + "\x06source\x18\x02 \x01(\tR\x06source\x12\x14\n" + + "\x05label\x18\x03 \x01(\tR\x05label\x12\x16\n" + + "\x06detail\x18\x04 \x01(\tR\x06detail\"k\n" + + "\x15SearchEntitiesRequest\x12&\n" + + "\x04type\x18\x01 \x01(\x0e2\x12.api.v1.MarkerTypeR\x04type\x12\x14\n" + + "\x05query\x18\x02 \x01(\tR\x05query\x12\x14\n" + + "\x05limit\x18\x03 \x01(\x05R\x05limit\"D\n" + + "\x16SearchEntitiesResponse\x12*\n" + + "\bentities\x18\x01 \x03(\v2\x0e.api.v1.EntityR\bentities\"c\n" + + "\x0eReferenceQuery\x12\x15\n" + + "\x06ref_id\x18\x01 \x01(\tR\x05refId\x12&\n" + + "\x04type\x18\x02 \x01(\x0e2\x12.api.v1.MarkerTypeR\x04type\x12\x12\n" + + "\x04text\x18\x03 \x01(\tR\x04text\"n\n" + + "\x11ResolvedReference\x12\x15\n" + + "\x06ref_id\x18\x01 \x01(\tR\x05refId\x12\x1a\n" + + "\bresolved\x18\x02 \x01(\bR\bresolved\x12&\n" + + "\x06entity\x18\x03 \x01(\v2\x0e.api.v1.EntityR\x06entity\"L\n" + + "\x18ResolveReferencesRequest\x120\n" + + "\aqueries\x18\x01 \x03(\v2\x16.api.v1.ReferenceQueryR\aqueries\"P\n" + + "\x19ResolveReferencesResponse\x123\n" + + "\aresults\x18\x01 \x03(\v2\x19.api.v1.ResolvedReferenceR\aresults\"'\n" + + "\x11GetMarkersRequest\x12\x12\n" + + "\x04VXID\x18\x01 \x01(\tR\x04VXID\">\n" + + "\x12GetMarkersResponse\x12(\n" + + "\amarkers\x18\x01 \x03(\v2\x0e.api.v1.MarkerR\amarkers\"T\n" + + "\x14SubmitMarkersRequest\x12\x12\n" + + "\x04VXID\x18\x01 \x01(\tR\x04VXID\x12(\n" + + "\amarkers\x18\x02 \x03(\v2\x0e.api.v1.MarkerR\amarkers*1\n" + "\x0eBmmEnvironment\x12\x0e\n" + "\n" + "Production\x10\x00\x12\x0f\n" + @@ -3764,7 +4563,19 @@ const file_api_v1_api_proto_rawDesc = "" + "\x16CANTEMO_ACTION_PREVIEW\x10\x01\x12\x1d\n" + "\x19CANTEMO_ACTION_TRANSCRIBE\x10\x02\x12)\n" + "%CANTEMO_ACTION_SUBTITLE_FROM_SUBTRANS\x10\x03\x12#\n" + - "\x1fCANTEMO_ACTION_UPDATE_RELATIONS\x10\x042\xe3\r\n" + + "\x1fCANTEMO_ACTION_UPDATE_RELATIONS\x10\x04*\xa9\x01\n" + + "\n" + + "MarkerType\x12\x1b\n" + + "\x17MARKER_TYPE_UNSPECIFIED\x10\x00\x12\x1a\n" + + "\x16MARKER_TYPE_NAME_SUPER\x10\x01\x12\x1b\n" + + "\x17MARKER_TYPE_BIBLE_VERSE\x10\x02\x12\x14\n" + + "\x10MARKER_TYPE_SONG\x10\x03\x12\x17\n" + + "\x13MARKER_TYPE_CHAPTER\x10\x04\x12\x16\n" + + "\x12MARKER_TYPE_CUSTOM\x10\x05*c\n" + + "\fMarkerSource\x12\x1d\n" + + "\x19MARKER_SOURCE_UNSPECIFIED\x10\x00\x12\x1a\n" + + "\x16MARKER_SOURCE_IMPORTED\x10\x01\x12\x18\n" + + "\x14MARKER_SOURCE_MANUAL\x10\x022\x98\x10\n" + "\n" + "APIService\x125\n" + "\x0eGetPermissions\x12\f.api.v1.Void\x1a\x13.api.v1.Permissions\"\x00\x12B\n" + @@ -3791,7 +4602,12 @@ const file_api_v1_api_proto_rawDesc = "" + "\x15GetExportDestinations\x12\f.api.v1.Void\x1a\".api.v1.ExportDestinationsResponse\"\x00\x12K\n" + "\x14TriggerCantemoAction\x12#.api.v1.TriggerCantemoActionRequest\x1a\f.api.v1.Void\"\x00\x12H\n" + "\vVaultSearch\x12\x1a.api.v1.VaultSearchRequest\x1a\x1b.api.v1.VaultSearchResponse\"\x00\x12K\n" + - "\fGetVaultItem\x12\x1b.api.v1.GetVaultItemRequest\x1a\x1c.api.v1.GetVaultItemResponse\"\x00B\x1eZ\x1cbcc-media-tools/api/v1;apiv1b\x06proto3" + "\fGetVaultItem\x12\x1b.api.v1.GetVaultItemRequest\x1a\x1c.api.v1.GetVaultItemResponse\"\x00\x12E\n" + + "\n" + + "GetMarkers\x12\x19.api.v1.GetMarkersRequest\x1a\x1a.api.v1.GetMarkersResponse\"\x00\x12=\n" + + "\rSubmitMarkers\x12\x1c.api.v1.SubmitMarkersRequest\x1a\f.api.v1.Void\"\x00\x12Q\n" + + "\x0eSearchEntities\x12\x1d.api.v1.SearchEntitiesRequest\x1a\x1e.api.v1.SearchEntitiesResponse\"\x00\x12Z\n" + + "\x11ResolveReferences\x12 .api.v1.ResolveReferencesRequest\x1a!.api.v1.ResolveReferencesResponse\"\x00B\x1eZ\x1cbcc-media-tools/api/v1;apiv1b\x06proto3" var ( file_api_v1_api_proto_rawDescOnce sync.Once @@ -3805,162 +4621,193 @@ func file_api_v1_api_proto_rawDescGZIP() []byte { return file_api_v1_api_proto_rawDescData } -var file_api_v1_api_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_api_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 59) +var file_api_v1_api_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_api_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 70) var file_api_v1_api_proto_goTypes = []any{ (BmmEnvironment)(0), // 0: api.v1.BmmEnvironment (CantemoAction)(0), // 1: api.v1.CantemoAction - (*BMMPermission)(nil), // 2: api.v1.BMMPermission - (*TranscriptionPermission)(nil), // 3: api.v1.TranscriptionPermission - (*ExportPermission)(nil), // 4: api.v1.ExportPermission - (*VBExportPermission)(nil), // 5: api.v1.VBExportPermission - (*CantemoPermission)(nil), // 6: api.v1.CantemoPermission - (*VaultPermission)(nil), // 7: api.v1.VaultPermission - (*Permissions)(nil), // 8: api.v1.Permissions - (*GetPermissionsRequest)(nil), // 9: api.v1.GetPermissionsRequest - (*SetPermissionsRequest)(nil), // 10: api.v1.SetPermissionsRequest - (*DeletePermissionsRequest)(nil), // 11: api.v1.DeletePermissionsRequest - (*PermissionsList)(nil), // 12: api.v1.PermissionsList - (*BMMYear)(nil), // 13: api.v1.BMMYear - (*GetYearsResponse)(nil), // 14: api.v1.GetYearsResponse - (*GetYearsRequest)(nil), // 15: api.v1.GetYearsRequest - (*GetAlbumsRequest)(nil), // 16: api.v1.GetAlbumsRequest - (*Album)(nil), // 17: api.v1.Album - (*AlbumsList)(nil), // 18: api.v1.AlbumsList - (*GetAlbumTracksRequest)(nil), // 19: api.v1.GetAlbumTracksRequest - (*GetPodcastTracksRequest)(nil), // 20: api.v1.GetPodcastTracksRequest - (*GetAvailableLanguagesRequest)(nil), // 21: api.v1.GetAvailableLanguagesRequest - (*BMMTrack)(nil), // 22: api.v1.BMMTrack - (*TracksList)(nil), // 23: api.v1.TracksList - (*LanguageList)(nil), // 24: api.v1.LanguageList - (*Language)(nil), // 25: api.v1.Language - (*GetTranscriptionReqest)(nil), // 26: api.v1.GetTranscriptionReqest - (*Transcription)(nil), // 27: api.v1.Transcription - (*Segments)(nil), // 28: api.v1.Segments - (*Words)(nil), // 29: api.v1.Words - (*GetPreviewRequest)(nil), // 30: api.v1.GetPreviewRequest - (*Preview)(nil), // 31: api.v1.Preview - (*GetBMMTranscriptionRequest)(nil), // 32: api.v1.GetBMMTranscriptionRequest - (*SubmitTranscriptionRequest)(nil), // 33: api.v1.SubmitTranscriptionRequest - (*SubmitShortRequest)(nil), // 34: api.v1.SubmitShortRequest - (*ExportResolution)(nil), // 35: api.v1.ExportResolution - (*ExportLanguage)(nil), // 36: api.v1.ExportLanguage - (*ExportSubclip)(nil), // 37: api.v1.ExportSubclip - (*GetExportConfigRequest)(nil), // 38: api.v1.GetExportConfigRequest - (*GetExportConfigResponse)(nil), // 39: api.v1.GetExportConfigResponse - (*ExportResolutionSelection)(nil), // 40: api.v1.ExportResolutionSelection - (*StartExportRequest)(nil), // 41: api.v1.StartExportRequest - (*StartExportResponse)(nil), // 42: api.v1.StartExportResponse - (*ExportTimedMetadataRequest)(nil), // 43: api.v1.ExportTimedMetadataRequest - (*GetVBExportConfigRequest)(nil), // 44: api.v1.GetVBExportConfigRequest - (*GetVBExportConfigResponse)(nil), // 45: api.v1.GetVBExportConfigResponse - (*StartVBExportRequest)(nil), // 46: api.v1.StartVBExportRequest - (*StartVBExportResponse)(nil), // 47: api.v1.StartVBExportResponse - (*ExportDestinationsResponse)(nil), // 48: api.v1.ExportDestinationsResponse - (*ResolveAssetsRequest)(nil), // 49: api.v1.ResolveAssetsRequest - (*ResolvedAsset)(nil), // 50: api.v1.ResolvedAsset - (*ResolveAssetsResponse)(nil), // 51: api.v1.ResolveAssetsResponse - (*TriggerCantemoActionRequest)(nil), // 52: api.v1.TriggerCantemoActionRequest - (*VaultSearchRequest)(nil), // 53: api.v1.VaultSearchRequest - (*VaultItem)(nil), // 54: api.v1.VaultItem - (*VaultFacet)(nil), // 55: api.v1.VaultFacet - (*VaultSearchResponse)(nil), // 56: api.v1.VaultSearchResponse - (*GetVaultItemRequest)(nil), // 57: api.v1.GetVaultItemRequest - (*GetVaultItemResponse)(nil), // 58: api.v1.GetVaultItemResponse - nil, // 59: api.v1.PermissionsList.PermissionsEntry - nil, // 60: api.v1.GetYearsResponse.DataEntry - (*timestamppb.Timestamp)(nil), // 61: google.protobuf.Timestamp - (*Void)(nil), // 62: api.v1.Void + (MarkerType)(0), // 2: api.v1.MarkerType + (MarkerSource)(0), // 3: api.v1.MarkerSource + (*BMMPermission)(nil), // 4: api.v1.BMMPermission + (*TranscriptionPermission)(nil), // 5: api.v1.TranscriptionPermission + (*ExportPermission)(nil), // 6: api.v1.ExportPermission + (*VBExportPermission)(nil), // 7: api.v1.VBExportPermission + (*CantemoPermission)(nil), // 8: api.v1.CantemoPermission + (*VaultPermission)(nil), // 9: api.v1.VaultPermission + (*Permissions)(nil), // 10: api.v1.Permissions + (*GetPermissionsRequest)(nil), // 11: api.v1.GetPermissionsRequest + (*SetPermissionsRequest)(nil), // 12: api.v1.SetPermissionsRequest + (*DeletePermissionsRequest)(nil), // 13: api.v1.DeletePermissionsRequest + (*PermissionsList)(nil), // 14: api.v1.PermissionsList + (*BMMYear)(nil), // 15: api.v1.BMMYear + (*GetYearsResponse)(nil), // 16: api.v1.GetYearsResponse + (*GetYearsRequest)(nil), // 17: api.v1.GetYearsRequest + (*GetAlbumsRequest)(nil), // 18: api.v1.GetAlbumsRequest + (*Album)(nil), // 19: api.v1.Album + (*AlbumsList)(nil), // 20: api.v1.AlbumsList + (*GetAlbumTracksRequest)(nil), // 21: api.v1.GetAlbumTracksRequest + (*GetPodcastTracksRequest)(nil), // 22: api.v1.GetPodcastTracksRequest + (*GetAvailableLanguagesRequest)(nil), // 23: api.v1.GetAvailableLanguagesRequest + (*BMMTrack)(nil), // 24: api.v1.BMMTrack + (*TracksList)(nil), // 25: api.v1.TracksList + (*LanguageList)(nil), // 26: api.v1.LanguageList + (*Language)(nil), // 27: api.v1.Language + (*GetTranscriptionReqest)(nil), // 28: api.v1.GetTranscriptionReqest + (*Transcription)(nil), // 29: api.v1.Transcription + (*Segments)(nil), // 30: api.v1.Segments + (*Words)(nil), // 31: api.v1.Words + (*GetPreviewRequest)(nil), // 32: api.v1.GetPreviewRequest + (*Preview)(nil), // 33: api.v1.Preview + (*GetBMMTranscriptionRequest)(nil), // 34: api.v1.GetBMMTranscriptionRequest + (*SubmitTranscriptionRequest)(nil), // 35: api.v1.SubmitTranscriptionRequest + (*SubmitShortRequest)(nil), // 36: api.v1.SubmitShortRequest + (*ExportResolution)(nil), // 37: api.v1.ExportResolution + (*ExportLanguage)(nil), // 38: api.v1.ExportLanguage + (*ExportSubclip)(nil), // 39: api.v1.ExportSubclip + (*GetExportConfigRequest)(nil), // 40: api.v1.GetExportConfigRequest + (*GetExportConfigResponse)(nil), // 41: api.v1.GetExportConfigResponse + (*ExportResolutionSelection)(nil), // 42: api.v1.ExportResolutionSelection + (*StartExportRequest)(nil), // 43: api.v1.StartExportRequest + (*StartExportResponse)(nil), // 44: api.v1.StartExportResponse + (*ExportTimedMetadataRequest)(nil), // 45: api.v1.ExportTimedMetadataRequest + (*GetVBExportConfigRequest)(nil), // 46: api.v1.GetVBExportConfigRequest + (*GetVBExportConfigResponse)(nil), // 47: api.v1.GetVBExportConfigResponse + (*StartVBExportRequest)(nil), // 48: api.v1.StartVBExportRequest + (*StartVBExportResponse)(nil), // 49: api.v1.StartVBExportResponse + (*ExportDestinationsResponse)(nil), // 50: api.v1.ExportDestinationsResponse + (*ResolveAssetsRequest)(nil), // 51: api.v1.ResolveAssetsRequest + (*ResolvedAsset)(nil), // 52: api.v1.ResolvedAsset + (*ResolveAssetsResponse)(nil), // 53: api.v1.ResolveAssetsResponse + (*TriggerCantemoActionRequest)(nil), // 54: api.v1.TriggerCantemoActionRequest + (*VaultSearchRequest)(nil), // 55: api.v1.VaultSearchRequest + (*VaultItem)(nil), // 56: api.v1.VaultItem + (*VaultFacet)(nil), // 57: api.v1.VaultFacet + (*VaultSearchResponse)(nil), // 58: api.v1.VaultSearchResponse + (*GetVaultItemRequest)(nil), // 59: api.v1.GetVaultItemRequest + (*GetVaultItemResponse)(nil), // 60: api.v1.GetVaultItemResponse + (*Marker)(nil), // 61: api.v1.Marker + (*Entity)(nil), // 62: api.v1.Entity + (*SearchEntitiesRequest)(nil), // 63: api.v1.SearchEntitiesRequest + (*SearchEntitiesResponse)(nil), // 64: api.v1.SearchEntitiesResponse + (*ReferenceQuery)(nil), // 65: api.v1.ReferenceQuery + (*ResolvedReference)(nil), // 66: api.v1.ResolvedReference + (*ResolveReferencesRequest)(nil), // 67: api.v1.ResolveReferencesRequest + (*ResolveReferencesResponse)(nil), // 68: api.v1.ResolveReferencesResponse + (*GetMarkersRequest)(nil), // 69: api.v1.GetMarkersRequest + (*GetMarkersResponse)(nil), // 70: api.v1.GetMarkersResponse + (*SubmitMarkersRequest)(nil), // 71: api.v1.SubmitMarkersRequest + nil, // 72: api.v1.PermissionsList.PermissionsEntry + nil, // 73: api.v1.GetYearsResponse.DataEntry + (*timestamppb.Timestamp)(nil), // 74: google.protobuf.Timestamp + (*Void)(nil), // 75: api.v1.Void } var file_api_v1_api_proto_depIdxs = []int32{ - 2, // 0: api.v1.Permissions.bmm:type_name -> api.v1.BMMPermission - 3, // 1: api.v1.Permissions.transcription:type_name -> api.v1.TranscriptionPermission - 4, // 2: api.v1.Permissions.export:type_name -> api.v1.ExportPermission - 5, // 3: api.v1.Permissions.vb_export:type_name -> api.v1.VBExportPermission - 6, // 4: api.v1.Permissions.cantemo:type_name -> api.v1.CantemoPermission - 7, // 5: api.v1.Permissions.vault:type_name -> api.v1.VaultPermission - 8, // 6: api.v1.SetPermissionsRequest.permissions:type_name -> api.v1.Permissions - 59, // 7: api.v1.PermissionsList.permissions:type_name -> api.v1.PermissionsList.PermissionsEntry - 60, // 8: api.v1.GetYearsResponse.data:type_name -> api.v1.GetYearsResponse.DataEntry + 4, // 0: api.v1.Permissions.bmm:type_name -> api.v1.BMMPermission + 5, // 1: api.v1.Permissions.transcription:type_name -> api.v1.TranscriptionPermission + 6, // 2: api.v1.Permissions.export:type_name -> api.v1.ExportPermission + 7, // 3: api.v1.Permissions.vb_export:type_name -> api.v1.VBExportPermission + 8, // 4: api.v1.Permissions.cantemo:type_name -> api.v1.CantemoPermission + 9, // 5: api.v1.Permissions.vault:type_name -> api.v1.VaultPermission + 10, // 6: api.v1.SetPermissionsRequest.permissions:type_name -> api.v1.Permissions + 72, // 7: api.v1.PermissionsList.permissions:type_name -> api.v1.PermissionsList.PermissionsEntry + 73, // 8: api.v1.GetYearsResponse.data:type_name -> api.v1.GetYearsResponse.DataEntry 0, // 9: api.v1.GetYearsRequest.environment:type_name -> api.v1.BmmEnvironment 0, // 10: api.v1.GetAlbumsRequest.environment:type_name -> api.v1.BmmEnvironment - 17, // 11: api.v1.AlbumsList.albums:type_name -> api.v1.Album + 19, // 11: api.v1.AlbumsList.albums:type_name -> api.v1.Album 0, // 12: api.v1.GetAlbumTracksRequest.environment:type_name -> api.v1.BmmEnvironment 0, // 13: api.v1.GetPodcastTracksRequest.environment:type_name -> api.v1.BmmEnvironment 0, // 14: api.v1.GetAvailableLanguagesRequest.environment:type_name -> api.v1.BmmEnvironment - 61, // 15: api.v1.BMMTrack.publishedAt:type_name -> google.protobuf.Timestamp - 24, // 16: api.v1.BMMTrack.languages:type_name -> api.v1.LanguageList - 24, // 17: api.v1.BMMTrack.transcriptions:type_name -> api.v1.LanguageList - 22, // 18: api.v1.TracksList.tracks:type_name -> api.v1.BMMTrack - 25, // 19: api.v1.LanguageList.Languages:type_name -> api.v1.Language - 28, // 20: api.v1.Transcription.segments:type_name -> api.v1.Segments - 29, // 21: api.v1.Segments.words:type_name -> api.v1.Words + 74, // 15: api.v1.BMMTrack.publishedAt:type_name -> google.protobuf.Timestamp + 26, // 16: api.v1.BMMTrack.languages:type_name -> api.v1.LanguageList + 26, // 17: api.v1.BMMTrack.transcriptions:type_name -> api.v1.LanguageList + 24, // 18: api.v1.TracksList.tracks:type_name -> api.v1.BMMTrack + 27, // 19: api.v1.LanguageList.Languages:type_name -> api.v1.Language + 30, // 20: api.v1.Transcription.segments:type_name -> api.v1.Segments + 31, // 21: api.v1.Segments.words:type_name -> api.v1.Words 0, // 22: api.v1.GetBMMTranscriptionRequest.environment:type_name -> api.v1.BmmEnvironment - 27, // 23: api.v1.SubmitTranscriptionRequest.transcription:type_name -> api.v1.Transcription - 36, // 24: api.v1.GetExportConfigResponse.languages:type_name -> api.v1.ExportLanguage - 35, // 25: api.v1.GetExportConfigResponse.resolutions:type_name -> api.v1.ExportResolution - 37, // 26: api.v1.GetExportConfigResponse.subclips:type_name -> api.v1.ExportSubclip - 40, // 27: api.v1.StartExportRequest.resolutions:type_name -> api.v1.ExportResolutionSelection - 50, // 28: api.v1.ResolveAssetsResponse.assets:type_name -> api.v1.ResolvedAsset + 29, // 23: api.v1.SubmitTranscriptionRequest.transcription:type_name -> api.v1.Transcription + 38, // 24: api.v1.GetExportConfigResponse.languages:type_name -> api.v1.ExportLanguage + 37, // 25: api.v1.GetExportConfigResponse.resolutions:type_name -> api.v1.ExportResolution + 39, // 26: api.v1.GetExportConfigResponse.subclips:type_name -> api.v1.ExportSubclip + 42, // 27: api.v1.StartExportRequest.resolutions:type_name -> api.v1.ExportResolutionSelection + 52, // 28: api.v1.ResolveAssetsResponse.assets:type_name -> api.v1.ResolvedAsset 1, // 29: api.v1.TriggerCantemoActionRequest.action:type_name -> api.v1.CantemoAction - 54, // 30: api.v1.VaultSearchResponse.items:type_name -> api.v1.VaultItem - 55, // 31: api.v1.VaultSearchResponse.facets:type_name -> api.v1.VaultFacet - 54, // 32: api.v1.GetVaultItemResponse.item:type_name -> api.v1.VaultItem - 8, // 33: api.v1.PermissionsList.PermissionsEntry.value:type_name -> api.v1.Permissions - 13, // 34: api.v1.GetYearsResponse.DataEntry.value:type_name -> api.v1.BMMYear - 62, // 35: api.v1.APIService.GetPermissions:input_type -> api.v1.Void - 10, // 36: api.v1.APIService.UpdatePermissions:input_type -> api.v1.SetPermissionsRequest - 11, // 37: api.v1.APIService.DeletePermissions:input_type -> api.v1.DeletePermissionsRequest - 62, // 38: api.v1.APIService.ListPermissions:input_type -> api.v1.Void - 26, // 39: api.v1.APIService.GetTranscription:input_type -> api.v1.GetTranscriptionReqest - 30, // 40: api.v1.APIService.GetPreview:input_type -> api.v1.GetPreviewRequest - 33, // 41: api.v1.APIService.SubmitTranscription:input_type -> api.v1.SubmitTranscriptionRequest - 15, // 42: api.v1.APIService.GetYears:input_type -> api.v1.GetYearsRequest - 16, // 43: api.v1.APIService.GetAlbums:input_type -> api.v1.GetAlbumsRequest - 19, // 44: api.v1.APIService.GetAlbumTracks:input_type -> api.v1.GetAlbumTracksRequest - 20, // 45: api.v1.APIService.GetPodcastTracks:input_type -> api.v1.GetPodcastTracksRequest - 21, // 46: api.v1.APIService.GetLanguages:input_type -> api.v1.GetAvailableLanguagesRequest - 32, // 47: api.v1.APIService.GetBMMTranscription:input_type -> api.v1.GetBMMTranscriptionRequest - 34, // 48: api.v1.APIService.SubmitShort:input_type -> api.v1.SubmitShortRequest - 38, // 49: api.v1.APIService.GetExportConfig:input_type -> api.v1.GetExportConfigRequest - 41, // 50: api.v1.APIService.StartExport:input_type -> api.v1.StartExportRequest - 43, // 51: api.v1.APIService.ExportTimedMetadata:input_type -> api.v1.ExportTimedMetadataRequest - 49, // 52: api.v1.APIService.ResolveAssets:input_type -> api.v1.ResolveAssetsRequest - 44, // 53: api.v1.APIService.GetVBExportConfig:input_type -> api.v1.GetVBExportConfigRequest - 46, // 54: api.v1.APIService.StartVBExport:input_type -> api.v1.StartVBExportRequest - 62, // 55: api.v1.APIService.GetExportDestinations:input_type -> api.v1.Void - 52, // 56: api.v1.APIService.TriggerCantemoAction:input_type -> api.v1.TriggerCantemoActionRequest - 53, // 57: api.v1.APIService.VaultSearch:input_type -> api.v1.VaultSearchRequest - 57, // 58: api.v1.APIService.GetVaultItem:input_type -> api.v1.GetVaultItemRequest - 8, // 59: api.v1.APIService.GetPermissions:output_type -> api.v1.Permissions - 62, // 60: api.v1.APIService.UpdatePermissions:output_type -> api.v1.Void - 62, // 61: api.v1.APIService.DeletePermissions:output_type -> api.v1.Void - 12, // 62: api.v1.APIService.ListPermissions:output_type -> api.v1.PermissionsList - 27, // 63: api.v1.APIService.GetTranscription:output_type -> api.v1.Transcription - 31, // 64: api.v1.APIService.GetPreview:output_type -> api.v1.Preview - 62, // 65: api.v1.APIService.SubmitTranscription:output_type -> api.v1.Void - 14, // 66: api.v1.APIService.GetYears:output_type -> api.v1.GetYearsResponse - 18, // 67: api.v1.APIService.GetAlbums:output_type -> api.v1.AlbumsList - 23, // 68: api.v1.APIService.GetAlbumTracks:output_type -> api.v1.TracksList - 23, // 69: api.v1.APIService.GetPodcastTracks:output_type -> api.v1.TracksList - 24, // 70: api.v1.APIService.GetLanguages:output_type -> api.v1.LanguageList - 27, // 71: api.v1.APIService.GetBMMTranscription:output_type -> api.v1.Transcription - 62, // 72: api.v1.APIService.SubmitShort:output_type -> api.v1.Void - 39, // 73: api.v1.APIService.GetExportConfig:output_type -> api.v1.GetExportConfigResponse - 42, // 74: api.v1.APIService.StartExport:output_type -> api.v1.StartExportResponse - 62, // 75: api.v1.APIService.ExportTimedMetadata:output_type -> api.v1.Void - 51, // 76: api.v1.APIService.ResolveAssets:output_type -> api.v1.ResolveAssetsResponse - 45, // 77: api.v1.APIService.GetVBExportConfig:output_type -> api.v1.GetVBExportConfigResponse - 47, // 78: api.v1.APIService.StartVBExport:output_type -> api.v1.StartVBExportResponse - 48, // 79: api.v1.APIService.GetExportDestinations:output_type -> api.v1.ExportDestinationsResponse - 62, // 80: api.v1.APIService.TriggerCantemoAction:output_type -> api.v1.Void - 56, // 81: api.v1.APIService.VaultSearch:output_type -> api.v1.VaultSearchResponse - 58, // 82: api.v1.APIService.GetVaultItem:output_type -> api.v1.GetVaultItemResponse - 59, // [59:83] is the sub-list for method output_type - 35, // [35:59] is the sub-list for method input_type - 35, // [35:35] is the sub-list for extension type_name - 35, // [35:35] is the sub-list for extension extendee - 0, // [0:35] is the sub-list for field type_name + 56, // 30: api.v1.VaultSearchResponse.items:type_name -> api.v1.VaultItem + 57, // 31: api.v1.VaultSearchResponse.facets:type_name -> api.v1.VaultFacet + 56, // 32: api.v1.GetVaultItemResponse.item:type_name -> api.v1.VaultItem + 2, // 33: api.v1.Marker.type:type_name -> api.v1.MarkerType + 3, // 34: api.v1.Marker.source:type_name -> api.v1.MarkerSource + 2, // 35: api.v1.SearchEntitiesRequest.type:type_name -> api.v1.MarkerType + 62, // 36: api.v1.SearchEntitiesResponse.entities:type_name -> api.v1.Entity + 2, // 37: api.v1.ReferenceQuery.type:type_name -> api.v1.MarkerType + 62, // 38: api.v1.ResolvedReference.entity:type_name -> api.v1.Entity + 65, // 39: api.v1.ResolveReferencesRequest.queries:type_name -> api.v1.ReferenceQuery + 66, // 40: api.v1.ResolveReferencesResponse.results:type_name -> api.v1.ResolvedReference + 61, // 41: api.v1.GetMarkersResponse.markers:type_name -> api.v1.Marker + 61, // 42: api.v1.SubmitMarkersRequest.markers:type_name -> api.v1.Marker + 10, // 43: api.v1.PermissionsList.PermissionsEntry.value:type_name -> api.v1.Permissions + 15, // 44: api.v1.GetYearsResponse.DataEntry.value:type_name -> api.v1.BMMYear + 75, // 45: api.v1.APIService.GetPermissions:input_type -> api.v1.Void + 12, // 46: api.v1.APIService.UpdatePermissions:input_type -> api.v1.SetPermissionsRequest + 13, // 47: api.v1.APIService.DeletePermissions:input_type -> api.v1.DeletePermissionsRequest + 75, // 48: api.v1.APIService.ListPermissions:input_type -> api.v1.Void + 28, // 49: api.v1.APIService.GetTranscription:input_type -> api.v1.GetTranscriptionReqest + 32, // 50: api.v1.APIService.GetPreview:input_type -> api.v1.GetPreviewRequest + 35, // 51: api.v1.APIService.SubmitTranscription:input_type -> api.v1.SubmitTranscriptionRequest + 17, // 52: api.v1.APIService.GetYears:input_type -> api.v1.GetYearsRequest + 18, // 53: api.v1.APIService.GetAlbums:input_type -> api.v1.GetAlbumsRequest + 21, // 54: api.v1.APIService.GetAlbumTracks:input_type -> api.v1.GetAlbumTracksRequest + 22, // 55: api.v1.APIService.GetPodcastTracks:input_type -> api.v1.GetPodcastTracksRequest + 23, // 56: api.v1.APIService.GetLanguages:input_type -> api.v1.GetAvailableLanguagesRequest + 34, // 57: api.v1.APIService.GetBMMTranscription:input_type -> api.v1.GetBMMTranscriptionRequest + 36, // 58: api.v1.APIService.SubmitShort:input_type -> api.v1.SubmitShortRequest + 40, // 59: api.v1.APIService.GetExportConfig:input_type -> api.v1.GetExportConfigRequest + 43, // 60: api.v1.APIService.StartExport:input_type -> api.v1.StartExportRequest + 45, // 61: api.v1.APIService.ExportTimedMetadata:input_type -> api.v1.ExportTimedMetadataRequest + 51, // 62: api.v1.APIService.ResolveAssets:input_type -> api.v1.ResolveAssetsRequest + 46, // 63: api.v1.APIService.GetVBExportConfig:input_type -> api.v1.GetVBExportConfigRequest + 48, // 64: api.v1.APIService.StartVBExport:input_type -> api.v1.StartVBExportRequest + 75, // 65: api.v1.APIService.GetExportDestinations:input_type -> api.v1.Void + 54, // 66: api.v1.APIService.TriggerCantemoAction:input_type -> api.v1.TriggerCantemoActionRequest + 55, // 67: api.v1.APIService.VaultSearch:input_type -> api.v1.VaultSearchRequest + 59, // 68: api.v1.APIService.GetVaultItem:input_type -> api.v1.GetVaultItemRequest + 69, // 69: api.v1.APIService.GetMarkers:input_type -> api.v1.GetMarkersRequest + 71, // 70: api.v1.APIService.SubmitMarkers:input_type -> api.v1.SubmitMarkersRequest + 63, // 71: api.v1.APIService.SearchEntities:input_type -> api.v1.SearchEntitiesRequest + 67, // 72: api.v1.APIService.ResolveReferences:input_type -> api.v1.ResolveReferencesRequest + 10, // 73: api.v1.APIService.GetPermissions:output_type -> api.v1.Permissions + 75, // 74: api.v1.APIService.UpdatePermissions:output_type -> api.v1.Void + 75, // 75: api.v1.APIService.DeletePermissions:output_type -> api.v1.Void + 14, // 76: api.v1.APIService.ListPermissions:output_type -> api.v1.PermissionsList + 29, // 77: api.v1.APIService.GetTranscription:output_type -> api.v1.Transcription + 33, // 78: api.v1.APIService.GetPreview:output_type -> api.v1.Preview + 75, // 79: api.v1.APIService.SubmitTranscription:output_type -> api.v1.Void + 16, // 80: api.v1.APIService.GetYears:output_type -> api.v1.GetYearsResponse + 20, // 81: api.v1.APIService.GetAlbums:output_type -> api.v1.AlbumsList + 25, // 82: api.v1.APIService.GetAlbumTracks:output_type -> api.v1.TracksList + 25, // 83: api.v1.APIService.GetPodcastTracks:output_type -> api.v1.TracksList + 26, // 84: api.v1.APIService.GetLanguages:output_type -> api.v1.LanguageList + 29, // 85: api.v1.APIService.GetBMMTranscription:output_type -> api.v1.Transcription + 75, // 86: api.v1.APIService.SubmitShort:output_type -> api.v1.Void + 41, // 87: api.v1.APIService.GetExportConfig:output_type -> api.v1.GetExportConfigResponse + 44, // 88: api.v1.APIService.StartExport:output_type -> api.v1.StartExportResponse + 75, // 89: api.v1.APIService.ExportTimedMetadata:output_type -> api.v1.Void + 53, // 90: api.v1.APIService.ResolveAssets:output_type -> api.v1.ResolveAssetsResponse + 47, // 91: api.v1.APIService.GetVBExportConfig:output_type -> api.v1.GetVBExportConfigResponse + 49, // 92: api.v1.APIService.StartVBExport:output_type -> api.v1.StartVBExportResponse + 50, // 93: api.v1.APIService.GetExportDestinations:output_type -> api.v1.ExportDestinationsResponse + 75, // 94: api.v1.APIService.TriggerCantemoAction:output_type -> api.v1.Void + 58, // 95: api.v1.APIService.VaultSearch:output_type -> api.v1.VaultSearchResponse + 60, // 96: api.v1.APIService.GetVaultItem:output_type -> api.v1.GetVaultItemResponse + 70, // 97: api.v1.APIService.GetMarkers:output_type -> api.v1.GetMarkersResponse + 75, // 98: api.v1.APIService.SubmitMarkers:output_type -> api.v1.Void + 64, // 99: api.v1.APIService.SearchEntities:output_type -> api.v1.SearchEntitiesResponse + 68, // 100: api.v1.APIService.ResolveReferences:output_type -> api.v1.ResolveReferencesResponse + 73, // [73:101] is the sub-list for method output_type + 45, // [45:73] is the sub-list for method input_type + 45, // [45:45] is the sub-list for extension type_name + 45, // [45:45] is the sub-list for extension extendee + 0, // [0:45] is the sub-list for field type_name } func init() { file_api_v1_api_proto_init() } @@ -3974,8 +4821,8 @@ func file_api_v1_api_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_api_proto_rawDesc), len(file_api_v1_api_proto_rawDesc)), - NumEnums: 2, - NumMessages: 59, + NumEnums: 4, + NumMessages: 70, NumExtensions: 0, NumServices: 1, }, diff --git a/backend/api/v1/apiv1connect/api.connect.go b/backend/api/v1/apiv1connect/api.connect.go index 1d99b11..4f2baaf 100644 --- a/backend/api/v1/apiv1connect/api.connect.go +++ b/backend/api/v1/apiv1connect/api.connect.go @@ -97,6 +97,17 @@ const ( APIServiceVaultSearchProcedure = "/api.v1.APIService/VaultSearch" // APIServiceGetVaultItemProcedure is the fully-qualified name of the APIService's GetVaultItem RPC. APIServiceGetVaultItemProcedure = "/api.v1.APIService/GetVaultItem" + // APIServiceGetMarkersProcedure is the fully-qualified name of the APIService's GetMarkers RPC. + APIServiceGetMarkersProcedure = "/api.v1.APIService/GetMarkers" + // APIServiceSubmitMarkersProcedure is the fully-qualified name of the APIService's SubmitMarkers + // RPC. + APIServiceSubmitMarkersProcedure = "/api.v1.APIService/SubmitMarkers" + // APIServiceSearchEntitiesProcedure is the fully-qualified name of the APIService's SearchEntities + // RPC. + APIServiceSearchEntitiesProcedure = "/api.v1.APIService/SearchEntities" + // APIServiceResolveReferencesProcedure is the fully-qualified name of the APIService's + // ResolveReferences RPC. + APIServiceResolveReferencesProcedure = "/api.v1.APIService/ResolveReferences" ) // APIServiceClient is a client for the api.v1.APIService service. @@ -135,6 +146,13 @@ type APIServiceClient interface { // VAULT (Vidispine search) VaultSearch(context.Context, *connect.Request[v1.VaultSearchRequest]) (*connect.Response[v1.VaultSearchResponse], error) GetVaultItem(context.Context, *connect.Request[v1.GetVaultItemRequest]) (*connect.Response[v1.GetVaultItemResponse], error) + // Markers + GetMarkers(context.Context, *connect.Request[v1.GetMarkersRequest]) (*connect.Response[v1.GetMarkersResponse], error) + SubmitMarkers(context.Context, *connect.Request[v1.SubmitMarkersRequest]) (*connect.Response[v1.Void], error) + // Autocomplete for marker labels (bible passages, songs, people). + SearchEntities(context.Context, *connect.Request[v1.SearchEntitiesRequest]) (*connect.Response[v1.SearchEntitiesResponse], error) + // Bulk-resolve free-text marker labels to canonical entities. + ResolveReferences(context.Context, *connect.Request[v1.ResolveReferencesRequest]) (*connect.Response[v1.ResolveReferencesResponse], error) } // NewAPIServiceClient constructs a client for the api.v1.APIService service. By default, it uses @@ -292,6 +310,30 @@ func NewAPIServiceClient(httpClient connect.HTTPClient, baseURL string, opts ... connect.WithSchema(aPIServiceMethods.ByName("GetVaultItem")), connect.WithClientOptions(opts...), ), + getMarkers: connect.NewClient[v1.GetMarkersRequest, v1.GetMarkersResponse]( + httpClient, + baseURL+APIServiceGetMarkersProcedure, + connect.WithSchema(aPIServiceMethods.ByName("GetMarkers")), + connect.WithClientOptions(opts...), + ), + submitMarkers: connect.NewClient[v1.SubmitMarkersRequest, v1.Void]( + httpClient, + baseURL+APIServiceSubmitMarkersProcedure, + connect.WithSchema(aPIServiceMethods.ByName("SubmitMarkers")), + connect.WithClientOptions(opts...), + ), + searchEntities: connect.NewClient[v1.SearchEntitiesRequest, v1.SearchEntitiesResponse]( + httpClient, + baseURL+APIServiceSearchEntitiesProcedure, + connect.WithSchema(aPIServiceMethods.ByName("SearchEntities")), + connect.WithClientOptions(opts...), + ), + resolveReferences: connect.NewClient[v1.ResolveReferencesRequest, v1.ResolveReferencesResponse]( + httpClient, + baseURL+APIServiceResolveReferencesProcedure, + connect.WithSchema(aPIServiceMethods.ByName("ResolveReferences")), + connect.WithClientOptions(opts...), + ), } } @@ -321,6 +363,10 @@ type aPIServiceClient struct { triggerCantemoAction *connect.Client[v1.TriggerCantemoActionRequest, v1.Void] vaultSearch *connect.Client[v1.VaultSearchRequest, v1.VaultSearchResponse] getVaultItem *connect.Client[v1.GetVaultItemRequest, v1.GetVaultItemResponse] + getMarkers *connect.Client[v1.GetMarkersRequest, v1.GetMarkersResponse] + submitMarkers *connect.Client[v1.SubmitMarkersRequest, v1.Void] + searchEntities *connect.Client[v1.SearchEntitiesRequest, v1.SearchEntitiesResponse] + resolveReferences *connect.Client[v1.ResolveReferencesRequest, v1.ResolveReferencesResponse] } // GetPermissions calls api.v1.APIService.GetPermissions. @@ -443,6 +489,26 @@ func (c *aPIServiceClient) GetVaultItem(ctx context.Context, req *connect.Reques return c.getVaultItem.CallUnary(ctx, req) } +// GetMarkers calls api.v1.APIService.GetMarkers. +func (c *aPIServiceClient) GetMarkers(ctx context.Context, req *connect.Request[v1.GetMarkersRequest]) (*connect.Response[v1.GetMarkersResponse], error) { + return c.getMarkers.CallUnary(ctx, req) +} + +// SubmitMarkers calls api.v1.APIService.SubmitMarkers. +func (c *aPIServiceClient) SubmitMarkers(ctx context.Context, req *connect.Request[v1.SubmitMarkersRequest]) (*connect.Response[v1.Void], error) { + return c.submitMarkers.CallUnary(ctx, req) +} + +// SearchEntities calls api.v1.APIService.SearchEntities. +func (c *aPIServiceClient) SearchEntities(ctx context.Context, req *connect.Request[v1.SearchEntitiesRequest]) (*connect.Response[v1.SearchEntitiesResponse], error) { + return c.searchEntities.CallUnary(ctx, req) +} + +// ResolveReferences calls api.v1.APIService.ResolveReferences. +func (c *aPIServiceClient) ResolveReferences(ctx context.Context, req *connect.Request[v1.ResolveReferencesRequest]) (*connect.Response[v1.ResolveReferencesResponse], error) { + return c.resolveReferences.CallUnary(ctx, req) +} + // APIServiceHandler is an implementation of the api.v1.APIService service. type APIServiceHandler interface { // Permissions @@ -479,6 +545,13 @@ type APIServiceHandler interface { // VAULT (Vidispine search) VaultSearch(context.Context, *connect.Request[v1.VaultSearchRequest]) (*connect.Response[v1.VaultSearchResponse], error) GetVaultItem(context.Context, *connect.Request[v1.GetVaultItemRequest]) (*connect.Response[v1.GetVaultItemResponse], error) + // Markers + GetMarkers(context.Context, *connect.Request[v1.GetMarkersRequest]) (*connect.Response[v1.GetMarkersResponse], error) + SubmitMarkers(context.Context, *connect.Request[v1.SubmitMarkersRequest]) (*connect.Response[v1.Void], error) + // Autocomplete for marker labels (bible passages, songs, people). + SearchEntities(context.Context, *connect.Request[v1.SearchEntitiesRequest]) (*connect.Response[v1.SearchEntitiesResponse], error) + // Bulk-resolve free-text marker labels to canonical entities. + ResolveReferences(context.Context, *connect.Request[v1.ResolveReferencesRequest]) (*connect.Response[v1.ResolveReferencesResponse], error) } // NewAPIServiceHandler builds an HTTP handler from the service implementation. It returns the path @@ -632,6 +705,30 @@ func NewAPIServiceHandler(svc APIServiceHandler, opts ...connect.HandlerOption) connect.WithSchema(aPIServiceMethods.ByName("GetVaultItem")), connect.WithHandlerOptions(opts...), ) + aPIServiceGetMarkersHandler := connect.NewUnaryHandler( + APIServiceGetMarkersProcedure, + svc.GetMarkers, + connect.WithSchema(aPIServiceMethods.ByName("GetMarkers")), + connect.WithHandlerOptions(opts...), + ) + aPIServiceSubmitMarkersHandler := connect.NewUnaryHandler( + APIServiceSubmitMarkersProcedure, + svc.SubmitMarkers, + connect.WithSchema(aPIServiceMethods.ByName("SubmitMarkers")), + connect.WithHandlerOptions(opts...), + ) + aPIServiceSearchEntitiesHandler := connect.NewUnaryHandler( + APIServiceSearchEntitiesProcedure, + svc.SearchEntities, + connect.WithSchema(aPIServiceMethods.ByName("SearchEntities")), + connect.WithHandlerOptions(opts...), + ) + aPIServiceResolveReferencesHandler := connect.NewUnaryHandler( + APIServiceResolveReferencesProcedure, + svc.ResolveReferences, + connect.WithSchema(aPIServiceMethods.ByName("ResolveReferences")), + connect.WithHandlerOptions(opts...), + ) return "/api.v1.APIService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case APIServiceGetPermissionsProcedure: @@ -682,6 +779,14 @@ func NewAPIServiceHandler(svc APIServiceHandler, opts ...connect.HandlerOption) aPIServiceVaultSearchHandler.ServeHTTP(w, r) case APIServiceGetVaultItemProcedure: aPIServiceGetVaultItemHandler.ServeHTTP(w, r) + case APIServiceGetMarkersProcedure: + aPIServiceGetMarkersHandler.ServeHTTP(w, r) + case APIServiceSubmitMarkersProcedure: + aPIServiceSubmitMarkersHandler.ServeHTTP(w, r) + case APIServiceSearchEntitiesProcedure: + aPIServiceSearchEntitiesHandler.ServeHTTP(w, r) + case APIServiceResolveReferencesProcedure: + aPIServiceResolveReferencesHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -786,3 +891,19 @@ func (UnimplementedAPIServiceHandler) VaultSearch(context.Context, *connect.Requ func (UnimplementedAPIServiceHandler) GetVaultItem(context.Context, *connect.Request[v1.GetVaultItemRequest]) (*connect.Response[v1.GetVaultItemResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("api.v1.APIService.GetVaultItem is not implemented")) } + +func (UnimplementedAPIServiceHandler) GetMarkers(context.Context, *connect.Request[v1.GetMarkersRequest]) (*connect.Response[v1.GetMarkersResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("api.v1.APIService.GetMarkers is not implemented")) +} + +func (UnimplementedAPIServiceHandler) SubmitMarkers(context.Context, *connect.Request[v1.SubmitMarkersRequest]) (*connect.Response[v1.Void], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("api.v1.APIService.SubmitMarkers is not implemented")) +} + +func (UnimplementedAPIServiceHandler) SearchEntities(context.Context, *connect.Request[v1.SearchEntitiesRequest]) (*connect.Response[v1.SearchEntitiesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("api.v1.APIService.SearchEntities is not implemented")) +} + +func (UnimplementedAPIServiceHandler) ResolveReferences(context.Context, *connect.Request[v1.ResolveReferencesRequest]) (*connect.Response[v1.ResolveReferencesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("api.v1.APIService.ResolveReferences is not implemented")) +} diff --git a/backend/api/v1/common.pb.go b/backend/api/v1/common.pb.go index 982edaa..4f8681d 100644 --- a/backend/api/v1/common.pb.go +++ b/backend/api/v1/common.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.8 // protoc (unknown) // source: api/v1/common.proto diff --git a/backend/cmd/server/bible.go b/backend/cmd/server/bible.go new file mode 100644 index 0000000..c55e9f9 --- /dev/null +++ b/backend/cmd/server/bible.go @@ -0,0 +1,287 @@ +package main + +import ( + apiv1 "bcc-media-tools/api/v1" + "context" + "fmt" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/go-resty/resty/v2" +) + +// Bible-verse marker autocomplete, backed by BCC's bible server +// (https://bibleapi.bcc.media, source: github.com/bcc-code/bible-server). +// +// The bible server has no search/autocomplete endpoint — only "list books" and +// "get verse". So the autocomplete logic (parse a free-text reference, prefix- +// match the book) lives here, driven by the authoritative book list we fetch +// and cache from the server. Using the server means the book names come from +// the actual translation (BCC broadcasts nb-1930) rather than a bundled table. +// +// A book's `id` is the canonical, translation-independent code ("Jn", "Gen") +// used both as the verse-endpoint path segment and in the canonical verse +// format ("Jn 3/16", "1Sam 2/18-34"). That canonical ref becomes the marker's +// entity_id, so a linked verse resolves the same regardless of display language. + +const ( + defaultBibleBaseURL = "https://bibleapi.bcc.media/v1" + // nb-1930 (Norwegian, Bibelen 1930) — BCC's broadcast translation. + defaultBibleID = "nb-1930" + booksCacheTTL = 24 * time.Hour +) + +type bibleBook struct { + ID string `json:"id"` + Number int `json:"number"` + LongName string `json:"long_name"` + ShortName string `json:"short_name"` + + // Normalized match forms, precomputed once when the list is cached so a + // search doesn't re-normalize every book on every keystroke. + normLong string + normWords []string + normShort string + normID string +} + +// BibleClient fetches and caches the book list for a single translation and +// resolves free-text references against it. +type BibleClient struct { + bibleID string + rest *resty.Client + + mu sync.Mutex + books []bibleBook + booksExpiry time.Time +} + +func NewBibleClient(baseURL, bibleID string) *BibleClient { + if baseURL == "" { + baseURL = defaultBibleBaseURL + } + if bibleID == "" { + bibleID = defaultBibleID + } + rest := resty.New() + rest.SetBaseURL(strings.TrimRight(baseURL, "/")) + rest.SetHeader("Accept", "application/json") + rest.SetTimeout(8 * time.Second) + return &BibleClient{bibleID: bibleID, rest: rest} +} + +// getBooks returns the cached book list, refreshing it from the server when stale. +func (c *BibleClient) getBooks(ctx context.Context) ([]bibleBook, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.books != nil && time.Now().Before(c.booksExpiry) { + return c.books, nil + } + + var books []bibleBook + resp, err := c.rest.R(). + SetContext(ctx). + SetResult(&books). + Get(fmt.Sprintf("/%s/books", c.bibleID)) + if err != nil { + return nil, err + } + if resp.IsError() { + return nil, fmt.Errorf("bible server returned %s for %s", resp.Status(), resp.Request.URL) + } + + for i := range books { + b := &books[i] + b.normLong = normalizeBibleQuery(b.LongName) + b.normWords = strings.Fields(b.normLong) + b.normShort = normalizeBibleQuery(b.ShortName) + b.normID = normalizeBibleQuery(b.ID) + } + + c.books = books + c.booksExpiry = time.Now().Add(booksCacheTTL) + return books, nil +} + +var ( + bibleSpaceRe = regexp.MustCompile(`\s+`) + // Splits a reference into its book part and a trailing chapter[:verse[-verse]]. + // The book part is non-greedy so a leading number ("1 Johannes") stays with + // the book while the anchored trailing number is taken as the chapter. Both + // ":" and "/" are accepted as the chapter/verse separator. + bibleRefRe = regexp.MustCompile(`^(.*?)\s*(\d+)(?:[:/](\d+)(?:\s*-\s*(\d+))?)?$`) +) + +func normalizeBibleQuery(s string) string { + s = strings.ToLower(strings.TrimSpace(s)) + s = strings.ReplaceAll(s, ".", "") // drop abbreviation dots ("Joh." -> "joh") + return bibleSpaceRe.ReplaceAllString(s, " ") +} + +// bookPrimaryMatch reports whether the query prefix names this book by its full +// display name, short name, or canonical id — the strong signals. A book whose +// name merely contains the query as a later word (e.g. "Johannes" in "Johannes’ +// åpenbaring") is deliberately excluded. +func bookPrimaryMatch(b bibleBook, q string) bool { + return strings.HasPrefix(b.normLong, q) || + strings.HasPrefix(b.normShort, q) || + strings.HasPrefix(b.normID, q) +} + +// bookMatchTier scores how strongly the query names this book: 2 for an exact +// short-name/id/long-name hit, 1 for a prefix (primary) hit, 0 otherwise. The +// tiers let Resolve prefer the exact match — "joh" uniquely picks Johannes over +// "Johannes’ åpenbaring", which only shares the prefix. +func bookMatchTier(b bibleBook, q string) int { + if q == b.normShort || q == b.normID || q == b.normLong { + return 2 + } + if bookPrimaryMatch(b, q) { + return 1 + } + return 0 +} + +// bookMatches is the looser search predicate: a primary match, or any word of +// the display name (so "genesis" hits "1 Mosebok Genesis"). All match forms are +// precomputed in getBooks. +func bookMatches(b bibleBook, q string) bool { + if q == "" { + return true + } + if bookPrimaryMatch(b, q) { + return true + } + for _, word := range b.normWords { + if strings.HasPrefix(word, q) { + return true + } + } + return false +} + +// parsedRef is a bible reference split into its book part and numeric location. +type parsedRef struct { + bookPart string + chapter int + verseFrom int + verseTo int +} + +func parseBibleRef(normalizedQuery string) parsedRef { + if m := bibleRefRe.FindStringSubmatch(normalizedQuery); m != nil { + p := parsedRef{bookPart: strings.TrimSpace(m[1])} + p.chapter, _ = strconv.Atoi(m[2]) + if m[3] != "" { + p.verseFrom, _ = strconv.Atoi(m[3]) + } + if m[4] != "" { + p.verseTo, _ = strconv.Atoi(m[4]) + } + return p + } + // No trailing number — the whole query is a book-name prefix. + return parsedRef{bookPart: normalizedQuery} +} + +// Search turns a free-text reference into canonical suggestions. Examples: +// "joh 3:16", "1 kor 13:4-7", "salme 23", "genesis", "jn 3/16". +// +// NOTE: chapter/verse numbers aren't range-validated here (the book list +// doesn't carry chapter counts); an out-of-range reference simply won't resolve +// when fetched. Book identity, however, is validated against the real list. +func (c *BibleClient) Search(ctx context.Context, query string, limit int) ([]*apiv1.Entity, error) { + q := normalizeBibleQuery(query) + if q == "" { + return nil, nil + } + + books, err := c.getBooks(ctx) + if err != nil { + return nil, err + } + + ref := parseBibleRef(q) + results := make([]*apiv1.Entity, 0, limit) + for _, b := range books { + if !bookMatches(b, ref.bookPart) { + continue + } + results = append(results, bibleEntity(b, ref.chapter, ref.verseFrom, ref.verseTo)) + if len(results) >= limit { + break + } + } + return results, nil +} + +// Resolve returns the single canonical entity a free-text reference maps to, or +// nil when it can't be resolved confidently. Confidence requires a chapter (so +// a bare book name won't auto-link a verse marker) and exactly one book at the +// strongest match tier (so an ambiguous prefix like "jo 3:16" is left for +// manual review, while an exact "joh 3:16" resolves). +func (c *BibleClient) Resolve(ctx context.Context, text string) (*apiv1.Entity, error) { + q := normalizeBibleQuery(text) + if q == "" { + return nil, nil + } + + books, err := c.getBooks(ctx) + if err != nil { + return nil, err + } + + ref := parseBibleRef(q) + if ref.chapter == 0 { + return nil, nil + } + + var match *bibleBook + bestTier, tieCount := 0, 0 + for i := range books { + tier := bookMatchTier(books[i], ref.bookPart) + if tier == 0 { + continue + } + if tier > bestTier { + bestTier, tieCount, match = tier, 1, &books[i] + } else if tier == bestTier { + tieCount++ + } + } + if match == nil || tieCount != 1 { + return nil, nil // no match, or ambiguous at the strongest tier + } + return bibleEntity(*match, ref.chapter, ref.verseFrom, ref.verseTo), nil +} + +// bibleEntity builds the autocomplete entry. `label` is human display text +// (":" separator, translation name); `id` is the canonical machine ref ("/" +// separator, canonical book code) stored as Marker.entity_id. +func bibleEntity(b bibleBook, chapter, verseFrom, verseTo int) *apiv1.Entity { + var id, label string + switch { + case chapter == 0: + id = b.ID + label = b.LongName + case verseFrom == 0: + id = fmt.Sprintf("%s %d", b.ID, chapter) + label = fmt.Sprintf("%s %d", b.LongName, chapter) + case verseTo > verseFrom: + id = fmt.Sprintf("%s %d/%d-%d", b.ID, chapter, verseFrom, verseTo) + label = fmt.Sprintf("%s %d:%d-%d", b.LongName, chapter, verseFrom, verseTo) + default: + id = fmt.Sprintf("%s %d/%d", b.ID, chapter, verseFrom) + label = fmt.Sprintf("%s %d:%d", b.LongName, chapter, verseFrom) + } + return &apiv1.Entity{ + Id: id, + Source: "bible", + Label: label, + Detail: id, // show the canonical ref as secondary text + } +} diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index e742dfc..1a86b5f 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -56,6 +56,7 @@ type ApiServer struct { ExportAPI CantemoAPI VaultAPI + *MarkersAPI } func withCORS(connectHandler http.Handler) http.Handler { @@ -129,6 +130,8 @@ func main() { os.Getenv("VIDISPINE_USERNAME"), os.Getenv("VIDISPINE_PASSWORD"), ) + bibleClient := NewBibleClient(os.Getenv("BIBLE_API_URL"), os.Getenv("BIBLE_ID")) + markersAPI := NewMarkersAPI(bibleClient) // Dedicated Cantemo client for the VAULT preview proxy (same creds as the // transcription tool). @@ -142,6 +145,7 @@ func main() { ExportAPI: *exportAPI, CantemoAPI: *cantemoAPI, VaultAPI: *vaultAPI, + MarkersAPI: markersAPI, } if os.Getenv("STATIC_FILE_PATH") != "" { diff --git a/backend/cmd/server/markers.go b/backend/cmd/server/markers.go new file mode 100644 index 0000000..33f4d73 --- /dev/null +++ b/backend/cmd/server/markers.go @@ -0,0 +1,107 @@ +package main + +import ( + apiv1 "bcc-media-tools/api/v1" + "context" + "fmt" + "sync" + + "connectrpc.com/connect" + "github.com/bcc-code/mediabank-bridge/log" +) + +// MarkersAPI is a placeholder, in-memory implementation of the markers store so +// the frontend can develop against a real contract. +// +// TODO: replace the in-memory map + demo seed with the real integration: +// - GetMarkers should merge markers imported from the third-party timing +// program (name-supers, bible-verse references, …) with manually-created +// ones, keyed by VX-id. +// - SubmitMarkers should persist edits and reconcile IMPORTED markers with +// their source. +type MarkersAPI struct { + mu sync.Mutex + store map[string][]*apiv1.Marker + + bible *BibleClient +} + +func NewMarkersAPI(bible *BibleClient) *MarkersAPI { + return &MarkersAPI{ + store: map[string][]*apiv1.Marker{}, + bible: bible, + } +} + +func (m *MarkersAPI) GetMarkers(ctx context.Context, req *connect.Request[apiv1.GetMarkersRequest]) (*connect.Response[apiv1.GetMarkersResponse], error) { + m.mu.Lock() + defer m.mu.Unlock() + + return connect.NewResponse(&apiv1.GetMarkersResponse{Markers: m.store[req.Msg.VXID]}), nil +} + +func (m *MarkersAPI) SubmitMarkers(ctx context.Context, req *connect.Request[apiv1.SubmitMarkersRequest]) (*connect.Response[apiv1.Void], error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.store[req.Msg.VXID] = req.Msg.Markers + fmt.Printf("Stored %d markers for VXID %s\n", len(req.Msg.Markers), req.Msg.VXID) + + return connect.NewResponse(&apiv1.Void{}), nil +} + +const defaultEntitySearchLimit = 10 +const maxEntitySearchLimit = 25 + +// SearchEntities powers marker-label autocomplete. Today only bible-verse +// lookups are wired up (against the BCC bible server); song and people +// registries can be added as further cases without changing the contract. +func (m *MarkersAPI) SearchEntities(ctx context.Context, req *connect.Request[apiv1.SearchEntitiesRequest]) (*connect.Response[apiv1.SearchEntitiesResponse], error) { + limit := int(req.Msg.Limit) + if limit <= 0 { + limit = defaultEntitySearchLimit + } + if limit > maxEntitySearchLimit { + limit = maxEntitySearchLimit + } + + var entities []*apiv1.Entity + switch req.Msg.Type { + case apiv1.MarkerType_MARKER_TYPE_BIBLE_VERSE: + found, err := m.bible.Search(ctx, req.Msg.Query, limit) + if err != nil { + // Autocomplete is best-effort: log and return no suggestions rather + // than failing the request (the user can still type free text). + log.L.Warn().Err(err).Msg("bible search failed") + break + } + entities = found + } + + return connect.NewResponse(&apiv1.SearchEntitiesResponse{Entities: entities}), nil +} + +// ResolveReferences bulk-resolves free-text marker labels to canonical entities +// for the "resolve references" action. Unresolvable entries come back with +// resolved=false; the whole request never fails on a per-entry lookup error. +func (m *MarkersAPI) ResolveReferences(ctx context.Context, req *connect.Request[apiv1.ResolveReferencesRequest]) (*connect.Response[apiv1.ResolveReferencesResponse], error) { + results := make([]*apiv1.ResolvedReference, 0, len(req.Msg.Queries)) + for _, q := range req.Msg.Queries { + res := &apiv1.ResolvedReference{RefId: q.RefId} + switch q.Type { + case apiv1.MarkerType_MARKER_TYPE_BIBLE_VERSE: + entity, err := m.bible.Resolve(ctx, q.Text) + if err != nil { + log.L.Warn().Err(err).Msg("bible resolve failed") + break + } + if entity != nil { + res.Resolved = true + res.Entity = entity + } + } + results = append(results, res) + } + + return connect.NewResponse(&apiv1.ResolveReferencesResponse{Results: results}), nil +} diff --git a/frontend/app/components/design/DesignCombobox.vue b/frontend/app/components/design/DesignCombobox.vue new file mode 100644 index 0000000..96827d7 --- /dev/null +++ b/frontend/app/components/design/DesignCombobox.vue @@ -0,0 +1,175 @@ + + + + + + {{ fieldLabel }} + + + + + + + + + + + + + + + + + + {{ item.label }} + + + + {{ item.detail }} + + + + + {{ emptyText }} + + + + + + diff --git a/frontend/app/components/markers/MarkersEditor.vue b/frontend/app/components/markers/MarkersEditor.vue new file mode 100644 index 0000000..0d02811 --- /dev/null +++ b/frontend/app/components/markers/MarkersEditor.vue @@ -0,0 +1,484 @@ + + + + + + + + {{ t("markers.editor.title") }} + + + {{ t(`markers.source.${marker.source}`) }} + + + + + + {{ t("markers.editor.type") }} + + + + + + + + + + + + + + + + + + + + + {{ t("markers.review.hint") }} + + + + + + + + + {{ t("markers.editor.timing") }} + + + {{ formatMarkerDuration(duration) }} + + + + + + + {{ t("markers.editor.in") }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t("markers.editor.out") }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t("markers.editor.previewRange") }} + + + + + + {{ t("markers.editor.remove") }} + + + + + + {{ t("markers.editor.noSelection") }} + + diff --git a/frontend/app/components/markers/MarkersListItem.vue b/frontend/app/components/markers/MarkersListItem.vue new file mode 100644 index 0000000..ee99a7f --- /dev/null +++ b/frontend/app/components/markers/MarkersListItem.vue @@ -0,0 +1,90 @@ + + + + + + + {{ displayLabel }} + + + + + + + {{ formatMarkerDuration(marker.end - marker.start) }} + + + diff --git a/frontend/app/components/markers/MarkersTimeline.vue b/frontend/app/components/markers/MarkersTimeline.vue new file mode 100644 index 0000000..a95836f --- /dev/null +++ b/frontend/app/components/markers/MarkersTimeline.vue @@ -0,0 +1,228 @@ + + + + + + {{ t("markers.timeline.empty") }} + + + + + + + + + + + + + + + + {{ laneLabel(lane.value) }} + + + + + + + + + + + {{ tick.label }} + + + + + + + + + + + {{ + marker.label || + laneLabel(marker.type) + }} + + + + + + + + + + + + + + diff --git a/frontend/app/composables/useMarkers.ts b/frontend/app/composables/useMarkers.ts new file mode 100644 index 0000000..3ac7839 --- /dev/null +++ b/frontend/app/composables/useMarkers.ts @@ -0,0 +1,155 @@ +import { + MarkerSource as PbMarkerSource, + MarkerType as PbMarkerType, + type Marker as PbMarker, +} from "~~/src/gen/api/v1/api_pb"; +import type { Marker, MarkerSource, MarkerType } from "~/utils/markers"; + +// Markers data layer, backed by the ConnectRPC API. Working state is held +// locally; `save` submits it. See `backend/cmd/server/markers.go` — the backend +// is still an in-memory placeholder pending the third-party timing integration. + +export const TYPE_TO_PB: Record = { + "name-super": PbMarkerType.NAME_SUPER, + "bible-verse": PbMarkerType.BIBLE_VERSE, + song: PbMarkerType.SONG, + chapter: PbMarkerType.CHAPTER, + custom: PbMarkerType.CUSTOM, +}; +const TYPE_FROM_PB: Record = { + [PbMarkerType.NAME_SUPER]: "name-super", + [PbMarkerType.BIBLE_VERSE]: "bible-verse", + [PbMarkerType.SONG]: "song", + [PbMarkerType.CHAPTER]: "chapter", + [PbMarkerType.CUSTOM]: "custom", +}; +const SOURCE_TO_PB: Record = { + imported: PbMarkerSource.IMPORTED, + manual: PbMarkerSource.MANUAL, +}; +const SOURCE_FROM_PB: Record = { + [PbMarkerSource.IMPORTED]: "imported", + [PbMarkerSource.MANUAL]: "manual", +}; + +function fromPb(m: PbMarker): Marker { + return { + id: m.id, + type: TYPE_FROM_PB[m.type] ?? "custom", + label: m.label, + note: m.note || undefined, + start: m.start, + end: m.end, + source: SOURCE_FROM_PB[m.source] ?? "manual", + entityId: m.entityId || undefined, + entitySource: m.entitySource || undefined, + }; +} + +function toPb(m: Marker) { + return { + id: m.id, + type: TYPE_TO_PB[m.type], + label: m.label, + note: m.note ?? "", + start: m.start, + end: m.end, + source: SOURCE_TO_PB[m.source], + entityId: m.entityId ?? "", + entitySource: m.entitySource ?? "", + }; +} + +export function useMarkers(vxId: MaybeRefOrGetter) { + const api = useAPI(); + + const markers = ref([]); + const loading = ref(true); + const error = ref(false); + // True when there are local changes not yet submitted (via `save`). + const dirty = ref(false); + + async function load() { + loading.value = true; + error.value = false; + try { + const res = await api.getMarkers({ VXID: toValue(vxId) }); + markers.value = res.markers.map(fromPb); + dirty.value = false; + } catch (err) { + console.error("Failed to load markers", err); + error.value = true; + } finally { + loading.value = false; + } + } + onMounted(load); + + function add( + partial: Partial & Pick, + ): Marker { + const marker: Marker = { + id: generateRandomId(), + type: "name-super", + label: "", + source: "manual", + ...partial, + }; + markers.value = [...markers.value, marker]; + dirty.value = true; + return marker; + } + + function update(id: string, patch: Partial>) { + markers.value = markers.value.map((m) => + m.id === id ? { ...m, ...patch } : m, + ); + dirty.value = true; + } + + function remove(id: string) { + markers.value = markers.value.filter((m) => m.id !== id); + dirty.value = true; + } + + function restore(marker: Marker) { + if (markers.value.some((m) => m.id === marker.id)) return; + markers.value = [...markers.value, marker]; + dirty.value = true; + } + + // Replace the whole working set (used by import). + function replaceAll(next: Marker[]) { + markers.value = next; + dirty.value = true; + } + + const saving = ref(false); + async function save() { + saving.value = true; + try { + await api.submitMarkers({ + VXID: toValue(vxId), + markers: markers.value.map(toPb), + }); + dirty.value = false; + } finally { + saving.value = false; + } + } + + return { + markers, + loading, + error, + reload: load, + dirty, + add, + update, + remove, + restore, + replaceAll, + save, + saving, + }; +} diff --git a/frontend/app/composables/useTools.ts b/frontend/app/composables/useTools.ts index b8de3f5..a7842a7 100644 --- a/frontend/app/composables/useTools.ts +++ b/frontend/app/composables/useTools.ts @@ -1,79 +1,96 @@ interface Tool { - label: string; - icon: string; - description: string; - to: string; - enabled?: boolean; + label: string; + icon: string; + description: string; + to: string; + enabled?: boolean; } export function useTools() { - const { t } = useI18n(); - const { me } = useMe(); - const route = useRoute() + const { t } = useI18n(); + const { me } = useMe(); + const route = useRoute(); - const tools = computed(() => [ - { - label: t("tools.bmmUpload.title"), - icon: "tabler:upload", - description: t("tools.bmmUpload.description"), - to: "/upload/bmm/", - enabled: me.value?.bmm && (me.value.bmm.podcasts.length > 0 || me.value.bmm.admin), - }, - { - label: t("tools.transcription.title"), - icon: "tabler:edit", - description: t("tools.transcription.description"), - to: "/transcription/", - enabled: me.value?.transcription && (me.value.transcription.mediabanken || me.value.transcription.admin), - }, - { - label: t("tools.export.title"), - icon: "tabler:file-export", - description: t("tools.export.description"), - to: "/export/", - enabled: me.value?.admin || (me.value?.export && (me.value.export.destinations.length > 0 || me.value.export.admin || me.value.export.timedMetadata)), - }, - { - label: t("tools.vbExport.title"), - icon: "tabler:broadcast", - description: t("tools.vbExport.description"), - to: "/vb-export/", - // Shown when the user has access to any VB destination (or is on the page). - enabled: - me.value?.admin || - (me.value?.vbExport && (me.value.vbExport.destinations.length > 0 || me.value.vbExport.admin)) || - route.path.startsWith("/vb-export"), - }, - { - label: 'Shorts generation', - icon: "tabler:device-mobile", - description: 'Generate shorts from existing videos', - to: "/shorts/", - }, - { - label: t("tools.vault.title"), - icon: "tabler:building-warehouse", - description: t("tools.vault.description"), - to: "/vault/", - enabled: me.value?.admin || me.value?.vault?.enabled, - }, - { - label: t("tools.admin.title"), - icon: "tabler:settings", - description: t("tools.admin.description"), - to: "/admin/", - enabled: me.value?.admin, - }, - ]); + const tools = computed(() => [ + { + label: t("tools.bmmUpload.title"), + icon: "tabler:upload", + description: t("tools.bmmUpload.description"), + to: "/upload/bmm/", + enabled: + me.value?.bmm && + (me.value.bmm.podcasts.length > 0 || me.value.bmm.admin), + }, + { + label: t("tools.transcription.title"), + icon: "tabler:edit", + description: t("tools.transcription.description"), + to: "/transcription/", + enabled: + me.value?.transcription && + (me.value.transcription.mediabanken || + me.value.transcription.admin), + }, + { + label: t("tools.export.title"), + icon: "tabler:file-export", + description: t("tools.export.description"), + to: "/export/", + enabled: + me.value?.admin || + (me.value?.export && + (me.value.export.destinations.length > 0 || + me.value.export.admin || + me.value.export.timedMetadata)), + }, + { + label: t("tools.vbExport.title"), + icon: "tabler:broadcast", + description: t("tools.vbExport.description"), + to: "/vb-export/", + // Shown when the user has access to any VB destination (or is on the page). + enabled: + me.value?.admin || + (me.value?.vbExport && + (me.value.vbExport.destinations.length > 0 || + me.value.vbExport.admin)) || + route.path.startsWith("/vb-export"), + }, + { + label: "Shorts generation", + icon: "tabler:device-mobile", + description: "Generate shorts from existing videos", + to: "/shorts/", + }, + { + label: t("tools.markers.title"), + icon: "tabler:bookmarks", + description: t("tools.markers.description"), + to: "/markers/", + }, + { + label: t("tools.vault.title"), + icon: "tabler:building-warehouse", + description: t("tools.vault.description"), + to: "/vault/", + enabled: me.value?.admin || me.value?.vault?.enabled, + }, + { + label: t("tools.admin.title"), + icon: "tabler:settings", + description: t("tools.admin.description"), + to: "/admin/", + enabled: me.value?.admin, + }, + ]); + const enabledTools = computed(() => + tools.value.filter((t) => t.enabled != false), + ); - const enabledTools = computed(() => - tools.value.filter((t) => t.enabled != false), - ); + const currentTool = computed(() => + tools.value.find((t) => route.path.startsWith(t.to)), + ); - const currentTool = computed(() => - tools.value.find((t) => route.path.startsWith(t.to)), - ); - - return { tools, enabledTools, currentTool }; + return { tools, enabledTools, currentTool }; } diff --git a/frontend/app/pages/markers/[id].vue b/frontend/app/pages/markers/[id].vue new file mode 100644 index 0000000..ba2ab84 --- /dev/null +++ b/frontend/app/pages/markers/[id].vue @@ -0,0 +1,683 @@ + + + + + + + {{ t("markers.editor.pageTitle") }} + {{ vxId }} + + + + + + {{ dirty ? t("markers.unsaved") : t("markers.allSaved") }} + + + + + + + {{ unresolvedCount }} + + + + + + + + + + + + + + + + + + + + {{ t("markers.save") }} + + + + + + + + + + + + {{ t("markers.previewUnavailable") }} + + + + + + + {{ m.label || t(`markers.types.${m.type}`) }} + + + + + + + + {{ formatMarkerTime(currentTime) }} + + + + {{ t("markers.addAtPlayhead") }} + + + + + {{ + t("markers.removed", { + label: + lastRemoved.label || + t(`markers.types.${lastRemoved.type}`), + }) + }} + + + {{ t("markers.undo") }} + + + + + + (selectedId = id)" + @seek="seek" + /> + + + + + + + + + + + + + {{ t("markers.loadError") }} + + + {{ t("markers.retry") }} + + + + + + + + + + + {{ t("markers.list.title") }} + {{ + visibleMarkers.length + }} + + + + + + + + + + + {{ t("markers.empty.title") }} + + + {{ t("markers.empty.hint") }} + + + + + + + + + + + {{ shortcut.label }} + + + + {{ key }} + + + + + + + diff --git a/frontend/app/pages/markers/index.vue b/frontend/app/pages/markers/index.vue new file mode 100644 index 0000000..c95f2f2 --- /dev/null +++ b/frontend/app/pages/markers/index.vue @@ -0,0 +1,56 @@ + + + + + + + + + {{ t("markers.index.title") }} + + + {{ t("markers.index.description") }} + + + + + + {{ t("markers.index.open") }} + + + + {{ t("markers.index.demoHint") }} + + + + diff --git a/frontend/app/utils/markers.ts b/frontend/app/utils/markers.ts new file mode 100644 index 0000000..b1ba734 --- /dev/null +++ b/frontend/app/utils/markers.ts @@ -0,0 +1,181 @@ +// Markers tool — data model. +// +// NOTE: this tool is currently frontend-only. Markers are persisted to +// localStorage by `useMarkers`. The shapes below are intentionally close to +// what a future ConnectRPC `GetMarkers` / `SubmitMarkers` pair would return so +// the mock data layer can be swapped for `api.*` calls with minimal churn. + +export type MarkerType = + | "name-super" + | "bible-verse" + | "song" + | "chapter" + | "custom"; + +// Where a marker came from. "imported" markers originate from the third-party +// timing program (name-supers, bible-verse references, …); "manual" ones are +// created here. The flag is preserved through edits so the future backend can +// reconcile imported markers with their source. +export type MarkerSource = "imported" | "manual"; + +export type Marker = { + id: string; + type: MarkerType; + // Display text: the name, the verse reference, the song/chapter title. + label: string; + note?: string; + // In/out points in seconds (float, ms precision — matches transcription Word). + start: number; + end: number; + source: MarkerSource; + // Canonical entity reference when the label is linked to a known entity (a + // bible passage, song, or person) — e.g. the bible ref "Jn 3/16". Empty for + // free-text / custom markers; `label` always holds the display text so + // linking stays optional enrichment. + entityId?: string; + // Which registry `entityId` belongs to: "bible" | "songbook" | "people". + entitySource?: string; +}; + +export type MarkerTypeMeta = { + value: MarkerType; + icon: string; + // Tailwind background class, used to fill the timeline blocks. + color: string; + // Tailwind text class, used to tint the type icon in the list / overlay. + iconColor: string; +}; + +// Single source of truth for the available marker types. Labels are resolved +// via i18n (`markers.types.`); see `markerTypeLabel`. +export const MARKER_TYPES: MarkerTypeMeta[] = [ + { + value: "name-super", + icon: "tabler:user", + color: "bg-blue-500", + iconColor: "text-blue-500", + }, + { + value: "bible-verse", + icon: "tabler:book-2", + color: "bg-purple-500", + iconColor: "text-purple-500", + }, + { + value: "song", + icon: "tabler:music", + color: "bg-emerald-500", + iconColor: "text-emerald-500", + }, + { + value: "chapter", + icon: "tabler:bookmark", + color: "bg-amber-500", + iconColor: "text-amber-500", + }, + { + value: "custom", + icon: "tabler:tag", + color: "bg-slate-500", + iconColor: "text-slate-500", + }, +]; + +export function markerTypeMeta(type: MarkerType): MarkerTypeMeta { + return ( + MARKER_TYPES.find((m) => m.value === type) ?? + MARKER_TYPES[MARKER_TYPES.length - 1]! + ); +} + +// Marker types whose labels can be linked to a canonical entity via the entity +// search / resolve backend. Grows as song and people registries come online. +export const LINKABLE_TYPES: ReadonlySet = new Set(["bible-verse"]); + +// A marker whose type should carry a canonical reference but doesn't yet — it +// has a label to resolve but no `entityId`. These are surfaced as "for review": +// auto-resolve couldn't confidently match them, so they need a manual look. +export function isMarkerUnresolved(m: Marker): boolean { + return LINKABLE_TYPES.has(m.type) && !!m.label.trim() && !m.entityId; +} + +// Sort by start time, then end time — stable order for the list and timeline. +export function sortMarkers(markers: Marker[]): Marker[] { + return [...markers].sort((a, b) => a.start - b.start || a.end - b.end); +} + +// ---- Import / export -------------------------------------------------------- + +export type MarkersFile = { + vxId: string; + markers: Marker[]; +}; + +export function serializeMarkers(vxId: string, markers: Marker[]): string { + const data: MarkersFile = { vxId, markers }; + return JSON.stringify(data, null, 2); +} + +// A non-empty string, or undefined — used to coerce optional imported fields. +const optStr = (v: unknown): string | undefined => + typeof v === "string" && v ? v : undefined; + +// Coerce an untrusted imported entry into a valid Marker, filling gaps. +function normalizeImportedMarker(raw: unknown): Marker { + const m = (raw ?? {}) as Record; + const type = MARKER_TYPES.some((t) => t.value === m.type) + ? (m.type as MarkerType) + : "custom"; + const start = Math.max(0, Number(m.start) || 0); + const end = Math.max(start, Number(m.end) || start); + return { + id: optStr(m.id) ?? generateRandomId(), + type, + label: typeof m.label === "string" ? m.label : "", + note: optStr(m.note), + start, + end, + source: m.source === "imported" ? "imported" : "manual", + entityId: optStr(m.entityId), + entitySource: optStr(m.entitySource), + }; +} + +// Parse an exported markers file — either a { markers: [...] } wrapper or a +// bare array — into validated markers. Throws if the shape is unusable. +export function parseMarkers(text: string): Marker[] { + const data = JSON.parse(text); + const raw = Array.isArray(data) ? data : (data?.markers ?? null); + if (!Array.isArray(raw)) throw new Error("Invalid markers file"); + return raw.map(normalizeImportedMarker); +} + +// Markers are shown/edited at whole-second granularity — ms precision isn't +// meaningful for on-screen graphics. (The shared `formatTime` keeps ms for the +// transcription editor; these are marker-specific.) +export function formatMarkerTime(seconds: number): string { + const s = Math.max(0, Math.round(seconds)); + const hh = Math.floor(s / 3600); + const mm = Math.floor((s % 3600) / 60); + const ss = s % 60; + return [hh, mm, ss].map((n) => n.toString().padStart(2, "0")).join(":"); +} + +// Compact marker length as M:SS (or H:MM:SS once it passes an hour). +export function formatMarkerDuration(seconds: number): string { + const s = Math.max(0, Math.round(seconds)); + const hh = Math.floor(s / 3600); + const mm = Math.floor((s % 3600) / 60); + const ss = s % 60; + const pad = (n: number) => n.toString().padStart(2, "0"); + return hh > 0 ? `${hh}:${pad(mm)}:${pad(ss)}` : `${mm}:${pad(ss)}`; +} + +// Parses "SS", "MM:SS" or "HH:MM:SS" into whole seconds; NaN if malformed. +export function parseMarkerTime(value: string): number { + const parts = value.trim().split(":"); + if (parts.length === 0 || parts.length > 3) return NaN; + const nums = parts.map(Number); + if (nums.some((n) => !Number.isFinite(n) || n < 0)) return NaN; + return nums.reduce((total, n) => total * 60 + n, 0); +} diff --git a/frontend/locales/en.json b/frontend/locales/en.json index cac00a3..1873302 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -192,6 +192,10 @@ "vault": { "title": "Vault", "description": "Search Mediabanken and preview items" + }, + "markers": { + "title": "Markers", + "description": "Add, edit and remove markers on a video" } }, "vault": { @@ -218,5 +222,94 @@ "audio": "Audio", "image": "Image" } + }, + "markers": { + "save": "Save", + "saved": "Markers saved", + "unsaved": "Unsaved changes", + "allSaved": "Saved", + "leaveConfirm": "You have unsaved changes. Leave without saving?", + "addAtPlayhead": "Add marker at playhead", + "addHint": "Press M to add a marker", + "previewUnavailable": "Preview unavailable", + "removed": "Removed \"{label}\"", + "undo": "Undo", + "import": "Import markers", + "export": "Export markers", + "importConfirm": "Replace the current markers with the imported file?", + "imported": "Imported {count} markers", + "importError": "Couldn't read that markers file", + "loadError": "Couldn't load markers", + "retry": "Retry", + "resolve": { + "action": "Resolve references", + "one": "Resolve", + "done": "Linked {linked} references", + "noMatch": "Couldn't find a confident match — pick it manually", + "error": "Couldn't resolve references" + }, + "review": { + "hint": "Not linked to a reference yet" + }, + "empty": { + "title": "No markers yet", + "hint": "Add a marker at the playhead, or press M" + }, + "shortcuts": { + "title": "Keyboard shortcuts", + "playPause": "Play / pause", + "seek": "Seek ±5 seconds", + "prevNext": "Previous / next marker", + "add": "Add marker at playhead", + "setInOut": "Set In / Out to playhead", + "remove": "Remove selected marker", + "help": "Show this help" + }, + "index": { + "title": "Markers", + "description": "Open a Cantemo asset by its VX-ID to manage markers.", + "open": "Open", + "demoHint": "Frontend-only preview — markers are stored in your browser." + }, + "types": { + "name-super": "Person", + "bible-verse": "Bible verse", + "song": "Song", + "chapter": "Chapter", + "custom": "Custom" + }, + "source": { + "imported": "Imported", + "manual": "Manual" + }, + "timeline": { + "empty": "No markers yet. Add one at the playhead." + }, + "list": { + "title": "Markers", + "empty": "No markers match the current filter." + }, + "editor": { + "pageTitle": "Markers ·", + "title": "Edit marker", + "type": "Type", + "label": "Label", + "labelPlaceholder": "e.g. John Doe or John 3:16", + "biblePlaceholder": "Search a passage, e.g. John 3:16", + "bibleEmpty": "No matching passage", + "note": "Note", + "notePlaceholder": "Optional note", + "in": "In", + "out": "Out", + "timing": "Timing", + "setToPlayhead": "Set to playhead", + "seekTo": "Go to", + "nudgeBack": "−1 second", + "nudgeForward": "+1 second", + "previewRange": "Preview range", + "duration": "Duration", + "remove": "Remove marker", + "noSelection": "Select a marker to edit, or add one at the playhead." + } } } diff --git a/frontend/locales/nb.json b/frontend/locales/nb.json index 9befe82..6e366d6 100644 --- a/frontend/locales/nb.json +++ b/frontend/locales/nb.json @@ -192,6 +192,10 @@ "vault": { "title": "Vault", "description": "Søk i Mediabanken og forhåndsvis elementer" + }, + "markers": { + "title": "Markører", + "description": "Legg til, rediger og fjern markører på en video" } }, "vault": { @@ -218,5 +222,94 @@ "audio": "Lyd", "image": "Bilde" } + }, + "markers": { + "save": "Lagre", + "saved": "Markører lagret", + "unsaved": "Ulagrede endringer", + "allSaved": "Lagret", + "leaveConfirm": "Du har ulagrede endringer. Forlate uten å lagre?", + "addAtPlayhead": "Legg til markør ved spillehodet", + "addHint": "Trykk M for å legge til en markør", + "previewUnavailable": "Forhåndsvisning utilgjengelig", + "removed": "Fjernet «{label}»", + "undo": "Angre", + "import": "Importer markører", + "export": "Eksporter markører", + "importConfirm": "Erstatt gjeldende markører med den importerte filen?", + "imported": "Importerte {count} markører", + "importError": "Kunne ikke lese markørfilen", + "loadError": "Kunne ikke laste markører", + "retry": "Prøv igjen", + "resolve": { + "action": "Koble referanser", + "one": "Koble", + "done": "Koblet {linked} referanser", + "noMatch": "Fant ingen sikker match — velg manuelt", + "error": "Kunne ikke koble referanser" + }, + "review": { + "hint": "Ikke koblet til en referanse ennå" + }, + "empty": { + "title": "Ingen markører ennå", + "hint": "Legg til en markør ved spillehodet, eller trykk M" + }, + "shortcuts": { + "title": "Hurtigtaster", + "playPause": "Spill / pause", + "seek": "Spol ±5 sekunder", + "prevNext": "Forrige / neste markør", + "add": "Legg til markør ved spillehodet", + "setInOut": "Sett inn / ut til spillehodet", + "remove": "Fjern valgt markør", + "help": "Vis denne hjelpen" + }, + "index": { + "title": "Markører", + "description": "Åpne et Cantemo-element med VX-ID for å håndtere markører.", + "open": "Åpne", + "demoHint": "Kun frontend — markører lagres i nettleseren din." + }, + "types": { + "name-super": "Person", + "bible-verse": "Bibelvers", + "song": "Sang", + "chapter": "Kapittel", + "custom": "Egendefinert" + }, + "source": { + "imported": "Importert", + "manual": "Manuell" + }, + "timeline": { + "empty": "Ingen markører ennå. Legg til en ved spillehodet." + }, + "list": { + "title": "Markører", + "empty": "Ingen markører samsvarer med filteret." + }, + "editor": { + "pageTitle": "Markører ·", + "title": "Rediger markør", + "type": "Type", + "label": "Etikett", + "labelPlaceholder": "f.eks. Ola Nordmann eller Joh 3:16", + "biblePlaceholder": "Søk etter et skriftsted, f.eks. Joh 3:16", + "bibleEmpty": "Fant ingen skriftsteder", + "note": "Notat", + "notePlaceholder": "Valgfritt notat", + "in": "Inn", + "out": "Ut", + "timing": "Tidsrom", + "setToPlayhead": "Sett til spillehodet", + "seekTo": "Gå til", + "nudgeBack": "−1 sekund", + "nudgeForward": "+1 sekund", + "previewRange": "Forhåndsvis utsnitt", + "duration": "Varighet", + "remove": "Fjern markør", + "noSelection": "Velg en markør for å redigere, eller legg til en ved spillehodet." + } } } diff --git a/frontend/src/gen/api/v1/api_pb.ts b/frontend/src/gen/api/v1/api_pb.ts index 3c3e908..d307100 100644 --- a/frontend/src/gen/api/v1/api_pb.ts +++ b/frontend/src/gen/api/v1/api_pb.ts @@ -14,7 +14,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file api/v1/api.proto. */ export const file_api_v1_api: GenFile = /*@__PURE__*/ - fileDesc("ChBhcGkvdjEvYXBpLnByb3RvEgZhcGkudjEiaAoNQk1NUGVybWlzc2lvbhIRCglsYW5ndWFnZXMYASADKAkSDgoGYWxidW1zGAIgAygJEhAKCHBvZGNhc3RzGAMgAygJEg0KBWFkbWluGAQgASgIEhMKC2ludGVncmF0aW9uGAUgASgIIj0KF1RyYW5zY3JpcHRpb25QZXJtaXNzaW9uEg0KBWFkbWluGAEgASgIEhMKC21lZGlhYmFua2VuGAIgASgIImQKEEV4cG9ydFBlcm1pc3Npb24SFAoMZGVzdGluYXRpb25zGAEgAygJEg0KBWFkbWluGAIgASgIEhYKDnRpbWVkX21ldGFkYXRhGAMgASgIEhMKC2J1bGtfZXhwb3J0GAQgASgIIk4KElZCRXhwb3J0UGVybWlzc2lvbhIUCgxkZXN0aW5hdGlvbnMYASADKAkSDQoFYWRtaW4YAiABKAgSEwoLYnVsa19leHBvcnQYAyABKAgiXgoRQ2FudGVtb1Blcm1pc3Npb24SDwoHcHJldmlldxgBIAEoCBISCgp0cmFuc2NyaWJlGAIgASgIEhEKCXN1YnRpdGxlcxgDIAEoCBIRCglyZWxhdGlvbnMYBCABKAgiIgoPVmF1bHRQZXJtaXNzaW9uEg8KB2VuYWJsZWQYASABKAgitAIKC1Blcm1pc3Npb25zEg0KBWFkbWluGAEgASgIEiIKA2JtbRgCIAEoCzIVLmFwaS52MS5CTU1QZXJtaXNzaW9uEg0KBWVtYWlsGAMgASgJEjYKDXRyYW5zY3JpcHRpb24YBCABKAsyHy5hcGkudjEuVHJhbnNjcmlwdGlvblBlcm1pc3Npb24SKAoGZXhwb3J0GAUgASgLMhguYXBpLnYxLkV4cG9ydFBlcm1pc3Npb24SLQoJdmJfZXhwb3J0GAYgASgLMhouYXBpLnYxLlZCRXhwb3J0UGVybWlzc2lvbhIqCgdjYW50ZW1vGAcgASgLMhkuYXBpLnYxLkNhbnRlbW9QZXJtaXNzaW9uEiYKBXZhdWx0GAggASgLMhcuYXBpLnYxLlZhdWx0UGVybWlzc2lvbiIXChVHZXRQZXJtaXNzaW9uc1JlcXVlc3QiUAoVU2V0UGVybWlzc2lvbnNSZXF1ZXN0Eg0KBWVtYWlsGAEgASgJEigKC3Blcm1pc3Npb25zGAIgASgLMhMuYXBpLnYxLlBlcm1pc3Npb25zIikKGERlbGV0ZVBlcm1pc3Npb25zUmVxdWVzdBINCgVlbWFpbBgBIAEoCSKZAQoPUGVybWlzc2lvbnNMaXN0Ej0KC3Blcm1pc3Npb25zGAEgAygLMiguYXBpLnYxLlBlcm1pc3Npb25zTGlzdC5QZXJtaXNzaW9uc0VudHJ5GkcKEFBlcm1pc3Npb25zRW50cnkSCwoDa2V5GAEgASgJEiIKBXZhbHVlGAIgASgLMhMuYXBpLnYxLlBlcm1pc3Npb25zOgI4ASImCgdCTU1ZZWFyEgwKBHllYXIYASABKA0SDQoFY291bnQYAiABKA0iggEKEEdldFllYXJzUmVzcG9uc2USMAoEZGF0YRgBIAMoCzIiLmFwaS52MS5HZXRZZWFyc1Jlc3BvbnNlLkRhdGFFbnRyeRo8CglEYXRhRW50cnkSCwoDa2V5GAEgASgNEh4KBXZhbHVlGAIgASgLMg8uYXBpLnYxLkJNTVllYXI6AjgBIj4KD0dldFllYXJzUmVxdWVzdBIrCgtlbnZpcm9ubWVudBgBIAEoDjIWLmFwaS52MS5CbW1FbnZpcm9ubWVudCJNChBHZXRBbGJ1bXNSZXF1ZXN0EgwKBHllYXIYASABKA0SKwoLZW52aXJvbm1lbnQYAiABKA4yFi5hcGkudjEuQm1tRW52aXJvbm1lbnQiRAoFQWxidW0SCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDQoFY292ZXIYBCABKAkSEQoJbGFuZ3VhZ2VzGAUgAygJIisKCkFsYnVtc0xpc3QSHQoGYWxidW1zGAEgAygLMg0uYXBpLnYxLkFsYnVtIlYKFUdldEFsYnVtVHJhY2tzUmVxdWVzdBIQCghhbGJ1bV9pZBgBIAEoCRIrCgtlbnZpcm9ubWVudBgCIAEoDjIWLmFwaS52MS5CbW1FbnZpcm9ubWVudCJqChdHZXRQb2RjYXN0VHJhY2tzUmVxdWVzdBITCgtwb2RjYXN0X3RhZxgBIAEoCRINCgVsaW1pdBgCIAEoDRIrCgtlbnZpcm9ubWVudBgDIAEoDjIWLmFwaS52MS5CbW1FbnZpcm9ubWVudCJLChxHZXRBdmFpbGFibGVMYW5ndWFnZXNSZXF1ZXN0EisKC2Vudmlyb25tZW50GAEgASgOMhYuYXBpLnYxLkJtbUVudmlyb25tZW50IsgBCghCTU1UcmFjaxIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRIvCgtwdWJsaXNoZWRBdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJwoJbGFuZ3VhZ2VzGAQgASgLMhQuYXBpLnYxLkxhbmd1YWdlTGlzdBIsCg50cmFuc2NyaXB0aW9ucxgFIAEoCzIULmFwaS52MS5MYW5ndWFnZUxpc3QSGQoRaGFzVHJhbnNjcmlwdGlvbnMYBiABKAgiLgoKVHJhY2tzTGlzdBIgCgZ0cmFja3MYASADKAsyEC5hcGkudjEuQk1NVHJhY2siMwoMTGFuZ3VhZ2VMaXN0EiMKCUxhbmd1YWdlcxgBIAMoCzIQLmFwaS52MS5MYW5ndWFnZSIrCghMYW5ndWFnZRIMCgRjb2RlGAEgASgJEhEKCWljb25fZmlsZRgDIAEoCSImChZHZXRUcmFuc2NyaXB0aW9uUmVxZXN0EgwKBFZYSUQYASABKAkiQQoNVHJhbnNjcmlwdGlvbhIMCgR0ZXh0GAEgASgJEiIKCHNlZ21lbnRzGAIgAygLMhAuYXBpLnYxLlNlZ21lbnRzIu0BCghTZWdtZW50cxIKCgJpZBgBIAEoARIMCgRzZWVrGAIgASgFEg0KBXN0YXJ0GAMgASgBEgsKA2VuZBgEIAEoARIMCgR0ZXh0GAUgASgJEg4KBnRva2VucxgGIAMoBRITCgt0ZW1wZXJhdHVyZRgHIAEoARITCgthdmdfbG9ncHJvYhgIIAEoARIZChFjb21wcmVzc2lvbl9yYXRpbxgJIAEoARIWCg5ub19zcGVlY2hfcHJvYhgKIAEoARISCgpjb25maWRlbmNlGAsgASgBEhwKBXdvcmRzGAwgAygLMg0uYXBpLnYxLldvcmRzIkUKBVdvcmRzEgwKBHRleHQYASABKAkSDQoFc3RhcnQYAiABKAESCwoDZW5kGAMgASgBEhIKCmNvbmZpZGVuY2UYBCABKAEiIQoRR2V0UHJldmlld1JlcXVlc3QSDAoEVlhJRBgBIAEoCSIWCgdQcmV2aWV3EgsKA3VybBgBIAEoCSJrChpHZXRCTU1UcmFuc2NyaXB0aW9uUmVxdWVzdBIOCgZibW1faWQYASABKAkSEAoIbGFuZ3VhZ2UYAiABKAkSKwoLZW52aXJvbm1lbnQYAyABKA4yFi5hcGkudjEuQm1tRW52aXJvbm1lbnQiWAoaU3VibWl0VHJhbnNjcmlwdGlvblJlcXVlc3QSDAoEVlhJRBgBIAEoCRIsCg10cmFuc2NyaXB0aW9uGAIgASgLMhUuYXBpLnYxLlRyYW5zY3JpcHRpb24iSQoSU3VibWl0U2hvcnRSZXF1ZXN0EgwKBFZYSUQYASABKAkSEQoJSW5TZWNvbmRzGAIgASgBEhIKCk91dFNlY29uZHMYAyABKAEiMQoQRXhwb3J0UmVzb2x1dGlvbhINCgV3aWR0aBgBIAEoBRIOCgZoZWlnaHQYAiABKAUiRgoORXhwb3J0TGFuZ3VhZ2USDAoEY29kZRgBIAEoCRIMCgRuYW1lGAIgASgJEgsKA211MRgDIAEoCBILCgNtdTIYBCABKAgiHgoNRXhwb3J0U3ViY2xpcBINCgV0aXRsZRgBIAEoCSImChZHZXRFeHBvcnRDb25maWdSZXF1ZXN0EgwKBFZYSUQYASABKAki1gIKF0dldEV4cG9ydENvbmZpZ1Jlc3BvbnNlEgwKBFZYSUQYASABKAkSDQoFdGl0bGUYAiABKAkSFAoMZGVzdGluYXRpb25zGAMgAygJEhUKDWF1ZGlvX3NvdXJjZXMYBCADKAkSHQoVc2VsZWN0ZWRfYXVkaW9fc291cmNlGAUgASgJEikKCWxhbmd1YWdlcxgGIAMoCzIWLmFwaS52MS5FeHBvcnRMYW5ndWFnZRIaChJzZWxlY3RlZF9sYW5ndWFnZXMYByADKAkSLQoLcmVzb2x1dGlvbnMYCCADKAsyGC5hcGkudjEuRXhwb3J0UmVzb2x1dGlvbhIQCghvdmVybGF5cxgJIAMoCRInCghzdWJjbGlwcxgKIAMoCzIVLmFwaS52MS5FeHBvcnRTdWJjbGlwEiEKGWNhbl9leHBvcnRfdGltZWRfbWV0YWRhdGEYCyABKAgiUAoZRXhwb3J0UmVzb2x1dGlvblNlbGVjdGlvbhINCgV3aWR0aBgBIAEoBRIOCgZoZWlnaHQYAiABKAUSFAoMZG93bmxvYWRhYmxlGAMgASgIIoMCChJTdGFydEV4cG9ydFJlcXVlc3QSDAoEVlhJRBgBIAEoCRIUCgxkZXN0aW5hdGlvbnMYAiADKAkSFAoMYXVkaW9fc291cmNlGAMgASgJEhEKCWxhbmd1YWdlcxgEIAMoCRI2CgtyZXNvbHV0aW9ucxgFIAMoCzIhLmFwaS52MS5FeHBvcnRSZXNvbHV0aW9uU2VsZWN0aW9uEg8KB292ZXJsYXkYBiABKAkSFQoNd2l0aF9jaGFwdGVycxgHIAEoCBIWCg5pZ25vcmVfc2lsZW5jZRgIIAEoCBIWCg5leHBvcnRfYWlfc3VicxgJIAEoCBIQCghzdWJjbGlwcxgKIAMoCSIrChNTdGFydEV4cG9ydFJlc3BvbnNlEhQKDHdvcmtmbG93X2lkcxgBIAMoCSIqChpFeHBvcnRUaW1lZE1ldGFkYXRhUmVxdWVzdBIMCgRWWElEGAEgASgJIigKGEdldFZCRXhwb3J0Q29uZmlnUmVxdWVzdBIMCgRWWElEGAEgASgJIoABChlHZXRWQkV4cG9ydENvbmZpZ1Jlc3BvbnNlEgwKBFZYSUQYASABKAkSDQoFdGl0bGUYAiABKAkSFAoMZGVzdGluYXRpb25zGAMgAygJEhcKD3N1YnRpdGxlX3NoYXBlcxgEIAMoCRIXCg9zdWJ0aXRsZV9zdHlsZXMYBSADKAkiagoUU3RhcnRWQkV4cG9ydFJlcXVlc3QSDAoEVlhJRBgBIAEoCRIUCgxkZXN0aW5hdGlvbnMYAiADKAkSFgoOc3VidGl0bGVfc2hhcGUYAyABKAkSFgoOc3VidGl0bGVfc3R5bGUYBCABKAkiLAoVU3RhcnRWQkV4cG9ydFJlc3BvbnNlEhMKC3dvcmtmbG93X2lkGAEgASgJIjQKGkV4cG9ydERlc3RpbmF0aW9uc1Jlc3BvbnNlEgoKAnZ4GAEgAygJEgoKAnZiGAIgAygJIiUKFFJlc29sdmVBc3NldHNSZXF1ZXN0Eg0KBVZYSURzGAEgAygJIjsKDVJlc29sdmVkQXNzZXQSDAoEVlhJRBgBIAEoCRINCgV0aXRsZRgCIAEoCRINCgVmb3VuZBgDIAEoCCI+ChVSZXNvbHZlQXNzZXRzUmVzcG9uc2USJQoGYXNzZXRzGAEgAygLMhUuYXBpLnYxLlJlc29sdmVkQXNzZXQiUgobVHJpZ2dlckNhbnRlbW9BY3Rpb25SZXF1ZXN0EgwKBFZYSUQYASABKAkSJQoGYWN0aW9uGAIgASgOMhUuYXBpLnYxLkNhbnRlbW9BY3Rpb24iRgoSVmF1bHRTZWFyY2hSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEhMKC21lZGlhX3R5cGVzGAIgAygJEgwKBHBhZ2UYAyABKAUimAEKCVZhdWx0SXRlbRIMCgRWWElEGAEgASgJEg0KBXRpdGxlGAIgASgJEhIKCm1lZGlhX3R5cGUYAyABKAkSDQoFYWRkZWQYBCABKAkSDgoGZm9ybWF0GAUgASgJEgwKBHNpemUYBiABKAkSGAoQZHVyYXRpb25fc2Vjb25kcxgHIAEoBRITCgtoYXNfcHJldmlldxgIIAEoCCIvCgpWYXVsdEZhY2V0EhIKCm1lZGlhX3R5cGUYASABKAkSDQoFY291bnQYAiABKAUikAEKE1ZhdWx0U2VhcmNoUmVzcG9uc2USIAoFaXRlbXMYASADKAsyES5hcGkudjEuVmF1bHRJdGVtEhIKCnRvdGFsX2hpdHMYAiABKAUSDAoEcGFnZRgDIAEoBRIRCglwYWdlX3NpemUYBCABKAUSIgoGZmFjZXRzGAUgAygLMhIuYXBpLnYxLlZhdWx0RmFjZXQiIwoTR2V0VmF1bHRJdGVtUmVxdWVzdBIMCgRWWElEGAEgASgJIjcKFEdldFZhdWx0SXRlbVJlc3BvbnNlEh8KBGl0ZW0YASABKAsyES5hcGkudjEuVmF1bHRJdGVtKjEKDkJtbUVudmlyb25tZW50Eg4KClByb2R1Y3Rpb24QABIPCgtJbnRlZ3JhdGlvbhABKroBCg1DYW50ZW1vQWN0aW9uEh4KGkNBTlRFTU9fQUNUSU9OX1VOU1BFQ0lGSUVEEAASGgoWQ0FOVEVNT19BQ1RJT05fUFJFVklFVxABEh0KGUNBTlRFTU9fQUNUSU9OX1RSQU5TQ1JJQkUQAhIpCiVDQU5URU1PX0FDVElPTl9TVUJUSVRMRV9GUk9NX1NVQlRSQU5TEAMSIwofQ0FOVEVNT19BQ1RJT05fVVBEQVRFX1JFTEFUSU9OUxAEMuMNCgpBUElTZXJ2aWNlEjUKDkdldFBlcm1pc3Npb25zEgwuYXBpLnYxLlZvaWQaEy5hcGkudjEuUGVybWlzc2lvbnMiABJCChFVcGRhdGVQZXJtaXNzaW9ucxIdLmFwaS52MS5TZXRQZXJtaXNzaW9uc1JlcXVlc3QaDC5hcGkudjEuVm9pZCIAEkUKEURlbGV0ZVBlcm1pc3Npb25zEiAuYXBpLnYxLkRlbGV0ZVBlcm1pc3Npb25zUmVxdWVzdBoMLmFwaS52MS5Wb2lkIgASOgoPTGlzdFBlcm1pc3Npb25zEgwuYXBpLnYxLlZvaWQaFy5hcGkudjEuUGVybWlzc2lvbnNMaXN0IgASSwoQR2V0VHJhbnNjcmlwdGlvbhIeLmFwaS52MS5HZXRUcmFuc2NyaXB0aW9uUmVxZXN0GhUuYXBpLnYxLlRyYW5zY3JpcHRpb24iABI6CgpHZXRQcmV2aWV3EhkuYXBpLnYxLkdldFByZXZpZXdSZXF1ZXN0Gg8uYXBpLnYxLlByZXZpZXciABJJChNTdWJtaXRUcmFuc2NyaXB0aW9uEiIuYXBpLnYxLlN1Ym1pdFRyYW5zY3JpcHRpb25SZXF1ZXN0GgwuYXBpLnYxLlZvaWQiABI/CghHZXRZZWFycxIXLmFwaS52MS5HZXRZZWFyc1JlcXVlc3QaGC5hcGkudjEuR2V0WWVhcnNSZXNwb25zZSIAEjsKCUdldEFsYnVtcxIYLmFwaS52MS5HZXRBbGJ1bXNSZXF1ZXN0GhIuYXBpLnYxLkFsYnVtc0xpc3QiABJFCg5HZXRBbGJ1bVRyYWNrcxIdLmFwaS52MS5HZXRBbGJ1bVRyYWNrc1JlcXVlc3QaEi5hcGkudjEuVHJhY2tzTGlzdCIAEkkKEEdldFBvZGNhc3RUcmFja3MSHy5hcGkudjEuR2V0UG9kY2FzdFRyYWNrc1JlcXVlc3QaEi5hcGkudjEuVHJhY2tzTGlzdCIAEkwKDEdldExhbmd1YWdlcxIkLmFwaS52MS5HZXRBdmFpbGFibGVMYW5ndWFnZXNSZXF1ZXN0GhQuYXBpLnYxLkxhbmd1YWdlTGlzdCIAElIKE0dldEJNTVRyYW5zY3JpcHRpb24SIi5hcGkudjEuR2V0Qk1NVHJhbnNjcmlwdGlvblJlcXVlc3QaFS5hcGkudjEuVHJhbnNjcmlwdGlvbiIAEjkKC1N1Ym1pdFNob3J0EhouYXBpLnYxLlN1Ym1pdFNob3J0UmVxdWVzdBoMLmFwaS52MS5Wb2lkIgASVAoPR2V0RXhwb3J0Q29uZmlnEh4uYXBpLnYxLkdldEV4cG9ydENvbmZpZ1JlcXVlc3QaHy5hcGkudjEuR2V0RXhwb3J0Q29uZmlnUmVzcG9uc2UiABJICgtTdGFydEV4cG9ydBIaLmFwaS52MS5TdGFydEV4cG9ydFJlcXVlc3QaGy5hcGkudjEuU3RhcnRFeHBvcnRSZXNwb25zZSIAEkkKE0V4cG9ydFRpbWVkTWV0YWRhdGESIi5hcGkudjEuRXhwb3J0VGltZWRNZXRhZGF0YVJlcXVlc3QaDC5hcGkudjEuVm9pZCIAEk4KDVJlc29sdmVBc3NldHMSHC5hcGkudjEuUmVzb2x2ZUFzc2V0c1JlcXVlc3QaHS5hcGkudjEuUmVzb2x2ZUFzc2V0c1Jlc3BvbnNlIgASWgoRR2V0VkJFeHBvcnRDb25maWcSIC5hcGkudjEuR2V0VkJFeHBvcnRDb25maWdSZXF1ZXN0GiEuYXBpLnYxLkdldFZCRXhwb3J0Q29uZmlnUmVzcG9uc2UiABJOCg1TdGFydFZCRXhwb3J0EhwuYXBpLnYxLlN0YXJ0VkJFeHBvcnRSZXF1ZXN0Gh0uYXBpLnYxLlN0YXJ0VkJFeHBvcnRSZXNwb25zZSIAEksKFUdldEV4cG9ydERlc3RpbmF0aW9ucxIMLmFwaS52MS5Wb2lkGiIuYXBpLnYxLkV4cG9ydERlc3RpbmF0aW9uc1Jlc3BvbnNlIgASSwoUVHJpZ2dlckNhbnRlbW9BY3Rpb24SIy5hcGkudjEuVHJpZ2dlckNhbnRlbW9BY3Rpb25SZXF1ZXN0GgwuYXBpLnYxLlZvaWQiABJICgtWYXVsdFNlYXJjaBIaLmFwaS52MS5WYXVsdFNlYXJjaFJlcXVlc3QaGy5hcGkudjEuVmF1bHRTZWFyY2hSZXNwb25zZSIAEksKDEdldFZhdWx0SXRlbRIbLmFwaS52MS5HZXRWYXVsdEl0ZW1SZXF1ZXN0GhwuYXBpLnYxLkdldFZhdWx0SXRlbVJlc3BvbnNlIgBCHlocYmNjLW1lZGlhLXRvb2xzL2FwaS92MTthcGl2MWIGcHJvdG8z", [file_google_protobuf_timestamp, file_api_v1_common]); + fileDesc("ChBhcGkvdjEvYXBpLnByb3RvEgZhcGkudjEiaAoNQk1NUGVybWlzc2lvbhIRCglsYW5ndWFnZXMYASADKAkSDgoGYWxidW1zGAIgAygJEhAKCHBvZGNhc3RzGAMgAygJEg0KBWFkbWluGAQgASgIEhMKC2ludGVncmF0aW9uGAUgASgIIj0KF1RyYW5zY3JpcHRpb25QZXJtaXNzaW9uEg0KBWFkbWluGAEgASgIEhMKC21lZGlhYmFua2VuGAIgASgIImQKEEV4cG9ydFBlcm1pc3Npb24SFAoMZGVzdGluYXRpb25zGAEgAygJEg0KBWFkbWluGAIgASgIEhYKDnRpbWVkX21ldGFkYXRhGAMgASgIEhMKC2J1bGtfZXhwb3J0GAQgASgIIk4KElZCRXhwb3J0UGVybWlzc2lvbhIUCgxkZXN0aW5hdGlvbnMYASADKAkSDQoFYWRtaW4YAiABKAgSEwoLYnVsa19leHBvcnQYAyABKAgiXgoRQ2FudGVtb1Blcm1pc3Npb24SDwoHcHJldmlldxgBIAEoCBISCgp0cmFuc2NyaWJlGAIgASgIEhEKCXN1YnRpdGxlcxgDIAEoCBIRCglyZWxhdGlvbnMYBCABKAgiIgoPVmF1bHRQZXJtaXNzaW9uEg8KB2VuYWJsZWQYASABKAgitAIKC1Blcm1pc3Npb25zEg0KBWFkbWluGAEgASgIEiIKA2JtbRgCIAEoCzIVLmFwaS52MS5CTU1QZXJtaXNzaW9uEg0KBWVtYWlsGAMgASgJEjYKDXRyYW5zY3JpcHRpb24YBCABKAsyHy5hcGkudjEuVHJhbnNjcmlwdGlvblBlcm1pc3Npb24SKAoGZXhwb3J0GAUgASgLMhguYXBpLnYxLkV4cG9ydFBlcm1pc3Npb24SLQoJdmJfZXhwb3J0GAYgASgLMhouYXBpLnYxLlZCRXhwb3J0UGVybWlzc2lvbhIqCgdjYW50ZW1vGAcgASgLMhkuYXBpLnYxLkNhbnRlbW9QZXJtaXNzaW9uEiYKBXZhdWx0GAggASgLMhcuYXBpLnYxLlZhdWx0UGVybWlzc2lvbiIXChVHZXRQZXJtaXNzaW9uc1JlcXVlc3QiUAoVU2V0UGVybWlzc2lvbnNSZXF1ZXN0Eg0KBWVtYWlsGAEgASgJEigKC3Blcm1pc3Npb25zGAIgASgLMhMuYXBpLnYxLlBlcm1pc3Npb25zIikKGERlbGV0ZVBlcm1pc3Npb25zUmVxdWVzdBINCgVlbWFpbBgBIAEoCSKZAQoPUGVybWlzc2lvbnNMaXN0Ej0KC3Blcm1pc3Npb25zGAEgAygLMiguYXBpLnYxLlBlcm1pc3Npb25zTGlzdC5QZXJtaXNzaW9uc0VudHJ5GkcKEFBlcm1pc3Npb25zRW50cnkSCwoDa2V5GAEgASgJEiIKBXZhbHVlGAIgASgLMhMuYXBpLnYxLlBlcm1pc3Npb25zOgI4ASImCgdCTU1ZZWFyEgwKBHllYXIYASABKA0SDQoFY291bnQYAiABKA0iggEKEEdldFllYXJzUmVzcG9uc2USMAoEZGF0YRgBIAMoCzIiLmFwaS52MS5HZXRZZWFyc1Jlc3BvbnNlLkRhdGFFbnRyeRo8CglEYXRhRW50cnkSCwoDa2V5GAEgASgNEh4KBXZhbHVlGAIgASgLMg8uYXBpLnYxLkJNTVllYXI6AjgBIj4KD0dldFllYXJzUmVxdWVzdBIrCgtlbnZpcm9ubWVudBgBIAEoDjIWLmFwaS52MS5CbW1FbnZpcm9ubWVudCJNChBHZXRBbGJ1bXNSZXF1ZXN0EgwKBHllYXIYASABKA0SKwoLZW52aXJvbm1lbnQYAiABKA4yFi5hcGkudjEuQm1tRW52aXJvbm1lbnQiRAoFQWxidW0SCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDQoFY292ZXIYBCABKAkSEQoJbGFuZ3VhZ2VzGAUgAygJIisKCkFsYnVtc0xpc3QSHQoGYWxidW1zGAEgAygLMg0uYXBpLnYxLkFsYnVtIlYKFUdldEFsYnVtVHJhY2tzUmVxdWVzdBIQCghhbGJ1bV9pZBgBIAEoCRIrCgtlbnZpcm9ubWVudBgCIAEoDjIWLmFwaS52MS5CbW1FbnZpcm9ubWVudCJqChdHZXRQb2RjYXN0VHJhY2tzUmVxdWVzdBITCgtwb2RjYXN0X3RhZxgBIAEoCRINCgVsaW1pdBgCIAEoDRIrCgtlbnZpcm9ubWVudBgDIAEoDjIWLmFwaS52MS5CbW1FbnZpcm9ubWVudCJLChxHZXRBdmFpbGFibGVMYW5ndWFnZXNSZXF1ZXN0EisKC2Vudmlyb25tZW50GAEgASgOMhYuYXBpLnYxLkJtbUVudmlyb25tZW50IsgBCghCTU1UcmFjaxIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRIvCgtwdWJsaXNoZWRBdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJwoJbGFuZ3VhZ2VzGAQgASgLMhQuYXBpLnYxLkxhbmd1YWdlTGlzdBIsCg50cmFuc2NyaXB0aW9ucxgFIAEoCzIULmFwaS52MS5MYW5ndWFnZUxpc3QSGQoRaGFzVHJhbnNjcmlwdGlvbnMYBiABKAgiLgoKVHJhY2tzTGlzdBIgCgZ0cmFja3MYASADKAsyEC5hcGkudjEuQk1NVHJhY2siMwoMTGFuZ3VhZ2VMaXN0EiMKCUxhbmd1YWdlcxgBIAMoCzIQLmFwaS52MS5MYW5ndWFnZSIrCghMYW5ndWFnZRIMCgRjb2RlGAEgASgJEhEKCWljb25fZmlsZRgDIAEoCSImChZHZXRUcmFuc2NyaXB0aW9uUmVxZXN0EgwKBFZYSUQYASABKAkiQQoNVHJhbnNjcmlwdGlvbhIMCgR0ZXh0GAEgASgJEiIKCHNlZ21lbnRzGAIgAygLMhAuYXBpLnYxLlNlZ21lbnRzIu0BCghTZWdtZW50cxIKCgJpZBgBIAEoARIMCgRzZWVrGAIgASgFEg0KBXN0YXJ0GAMgASgBEgsKA2VuZBgEIAEoARIMCgR0ZXh0GAUgASgJEg4KBnRva2VucxgGIAMoBRITCgt0ZW1wZXJhdHVyZRgHIAEoARITCgthdmdfbG9ncHJvYhgIIAEoARIZChFjb21wcmVzc2lvbl9yYXRpbxgJIAEoARIWCg5ub19zcGVlY2hfcHJvYhgKIAEoARISCgpjb25maWRlbmNlGAsgASgBEhwKBXdvcmRzGAwgAygLMg0uYXBpLnYxLldvcmRzIkUKBVdvcmRzEgwKBHRleHQYASABKAkSDQoFc3RhcnQYAiABKAESCwoDZW5kGAMgASgBEhIKCmNvbmZpZGVuY2UYBCABKAEiIQoRR2V0UHJldmlld1JlcXVlc3QSDAoEVlhJRBgBIAEoCSIWCgdQcmV2aWV3EgsKA3VybBgBIAEoCSJrChpHZXRCTU1UcmFuc2NyaXB0aW9uUmVxdWVzdBIOCgZibW1faWQYASABKAkSEAoIbGFuZ3VhZ2UYAiABKAkSKwoLZW52aXJvbm1lbnQYAyABKA4yFi5hcGkudjEuQm1tRW52aXJvbm1lbnQiWAoaU3VibWl0VHJhbnNjcmlwdGlvblJlcXVlc3QSDAoEVlhJRBgBIAEoCRIsCg10cmFuc2NyaXB0aW9uGAIgASgLMhUuYXBpLnYxLlRyYW5zY3JpcHRpb24iSQoSU3VibWl0U2hvcnRSZXF1ZXN0EgwKBFZYSUQYASABKAkSEQoJSW5TZWNvbmRzGAIgASgBEhIKCk91dFNlY29uZHMYAyABKAEiMQoQRXhwb3J0UmVzb2x1dGlvbhINCgV3aWR0aBgBIAEoBRIOCgZoZWlnaHQYAiABKAUiRgoORXhwb3J0TGFuZ3VhZ2USDAoEY29kZRgBIAEoCRIMCgRuYW1lGAIgASgJEgsKA211MRgDIAEoCBILCgNtdTIYBCABKAgiHgoNRXhwb3J0U3ViY2xpcBINCgV0aXRsZRgBIAEoCSImChZHZXRFeHBvcnRDb25maWdSZXF1ZXN0EgwKBFZYSUQYASABKAki1gIKF0dldEV4cG9ydENvbmZpZ1Jlc3BvbnNlEgwKBFZYSUQYASABKAkSDQoFdGl0bGUYAiABKAkSFAoMZGVzdGluYXRpb25zGAMgAygJEhUKDWF1ZGlvX3NvdXJjZXMYBCADKAkSHQoVc2VsZWN0ZWRfYXVkaW9fc291cmNlGAUgASgJEikKCWxhbmd1YWdlcxgGIAMoCzIWLmFwaS52MS5FeHBvcnRMYW5ndWFnZRIaChJzZWxlY3RlZF9sYW5ndWFnZXMYByADKAkSLQoLcmVzb2x1dGlvbnMYCCADKAsyGC5hcGkudjEuRXhwb3J0UmVzb2x1dGlvbhIQCghvdmVybGF5cxgJIAMoCRInCghzdWJjbGlwcxgKIAMoCzIVLmFwaS52MS5FeHBvcnRTdWJjbGlwEiEKGWNhbl9leHBvcnRfdGltZWRfbWV0YWRhdGEYCyABKAgiUAoZRXhwb3J0UmVzb2x1dGlvblNlbGVjdGlvbhINCgV3aWR0aBgBIAEoBRIOCgZoZWlnaHQYAiABKAUSFAoMZG93bmxvYWRhYmxlGAMgASgIIoMCChJTdGFydEV4cG9ydFJlcXVlc3QSDAoEVlhJRBgBIAEoCRIUCgxkZXN0aW5hdGlvbnMYAiADKAkSFAoMYXVkaW9fc291cmNlGAMgASgJEhEKCWxhbmd1YWdlcxgEIAMoCRI2CgtyZXNvbHV0aW9ucxgFIAMoCzIhLmFwaS52MS5FeHBvcnRSZXNvbHV0aW9uU2VsZWN0aW9uEg8KB292ZXJsYXkYBiABKAkSFQoNd2l0aF9jaGFwdGVycxgHIAEoCBIWCg5pZ25vcmVfc2lsZW5jZRgIIAEoCBIWCg5leHBvcnRfYWlfc3VicxgJIAEoCBIQCghzdWJjbGlwcxgKIAMoCSIrChNTdGFydEV4cG9ydFJlc3BvbnNlEhQKDHdvcmtmbG93X2lkcxgBIAMoCSIqChpFeHBvcnRUaW1lZE1ldGFkYXRhUmVxdWVzdBIMCgRWWElEGAEgASgJIigKGEdldFZCRXhwb3J0Q29uZmlnUmVxdWVzdBIMCgRWWElEGAEgASgJIoABChlHZXRWQkV4cG9ydENvbmZpZ1Jlc3BvbnNlEgwKBFZYSUQYASABKAkSDQoFdGl0bGUYAiABKAkSFAoMZGVzdGluYXRpb25zGAMgAygJEhcKD3N1YnRpdGxlX3NoYXBlcxgEIAMoCRIXCg9zdWJ0aXRsZV9zdHlsZXMYBSADKAkiagoUU3RhcnRWQkV4cG9ydFJlcXVlc3QSDAoEVlhJRBgBIAEoCRIUCgxkZXN0aW5hdGlvbnMYAiADKAkSFgoOc3VidGl0bGVfc2hhcGUYAyABKAkSFgoOc3VidGl0bGVfc3R5bGUYBCABKAkiLAoVU3RhcnRWQkV4cG9ydFJlc3BvbnNlEhMKC3dvcmtmbG93X2lkGAEgASgJIjQKGkV4cG9ydERlc3RpbmF0aW9uc1Jlc3BvbnNlEgoKAnZ4GAEgAygJEgoKAnZiGAIgAygJIiUKFFJlc29sdmVBc3NldHNSZXF1ZXN0Eg0KBVZYSURzGAEgAygJIjsKDVJlc29sdmVkQXNzZXQSDAoEVlhJRBgBIAEoCRINCgV0aXRsZRgCIAEoCRINCgVmb3VuZBgDIAEoCCI+ChVSZXNvbHZlQXNzZXRzUmVzcG9uc2USJQoGYXNzZXRzGAEgAygLMhUuYXBpLnYxLlJlc29sdmVkQXNzZXQiUgobVHJpZ2dlckNhbnRlbW9BY3Rpb25SZXF1ZXN0EgwKBFZYSUQYASABKAkSJQoGYWN0aW9uGAIgASgOMhUuYXBpLnYxLkNhbnRlbW9BY3Rpb24iRgoSVmF1bHRTZWFyY2hSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEhMKC21lZGlhX3R5cGVzGAIgAygJEgwKBHBhZ2UYAyABKAUimAEKCVZhdWx0SXRlbRIMCgRWWElEGAEgASgJEg0KBXRpdGxlGAIgASgJEhIKCm1lZGlhX3R5cGUYAyABKAkSDQoFYWRkZWQYBCABKAkSDgoGZm9ybWF0GAUgASgJEgwKBHNpemUYBiABKAkSGAoQZHVyYXRpb25fc2Vjb25kcxgHIAEoBRITCgtoYXNfcHJldmlldxgIIAEoCCIvCgpWYXVsdEZhY2V0EhIKCm1lZGlhX3R5cGUYASABKAkSDQoFY291bnQYAiABKAUikAEKE1ZhdWx0U2VhcmNoUmVzcG9uc2USIAoFaXRlbXMYASADKAsyES5hcGkudjEuVmF1bHRJdGVtEhIKCnRvdGFsX2hpdHMYAiABKAUSDAoEcGFnZRgDIAEoBRIRCglwYWdlX3NpemUYBCABKAUSIgoGZmFjZXRzGAUgAygLMhIuYXBpLnYxLlZhdWx0RmFjZXQiIwoTR2V0VmF1bHRJdGVtUmVxdWVzdBIMCgRWWElEGAEgASgJIjcKFEdldFZhdWx0SXRlbVJlc3BvbnNlEh8KBGl0ZW0YASABKAsyES5hcGkudjEuVmF1bHRJdGVtIr8BCgZNYXJrZXISCgoCaWQYASABKAkSIAoEdHlwZRgCIAEoDjISLmFwaS52MS5NYXJrZXJUeXBlEg0KBWxhYmVsGAMgASgJEgwKBG5vdGUYBCABKAkSDQoFc3RhcnQYBSABKAESCwoDZW5kGAYgASgBEiQKBnNvdXJjZRgHIAEoDjIULmFwaS52MS5NYXJrZXJTb3VyY2USEQoJZW50aXR5X2lkGAggASgJEhUKDWVudGl0eV9zb3VyY2UYCSABKAkiQwoGRW50aXR5EgoKAmlkGAEgASgJEg4KBnNvdXJjZRgCIAEoCRINCgVsYWJlbBgDIAEoCRIOCgZkZXRhaWwYBCABKAkiVwoVU2VhcmNoRW50aXRpZXNSZXF1ZXN0EiAKBHR5cGUYASABKA4yEi5hcGkudjEuTWFya2VyVHlwZRINCgVxdWVyeRgCIAEoCRINCgVsaW1pdBgDIAEoBSI6ChZTZWFyY2hFbnRpdGllc1Jlc3BvbnNlEiAKCGVudGl0aWVzGAEgAygLMg4uYXBpLnYxLkVudGl0eSJQCg5SZWZlcmVuY2VRdWVyeRIOCgZyZWZfaWQYASABKAkSIAoEdHlwZRgCIAEoDjISLmFwaS52MS5NYXJrZXJUeXBlEgwKBHRleHQYAyABKAkiVQoRUmVzb2x2ZWRSZWZlcmVuY2USDgoGcmVmX2lkGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEh4KBmVudGl0eRgDIAEoCzIOLmFwaS52MS5FbnRpdHkiQwoYUmVzb2x2ZVJlZmVyZW5jZXNSZXF1ZXN0EicKB3F1ZXJpZXMYASADKAsyFi5hcGkudjEuUmVmZXJlbmNlUXVlcnkiRwoZUmVzb2x2ZVJlZmVyZW5jZXNSZXNwb25zZRIqCgdyZXN1bHRzGAEgAygLMhkuYXBpLnYxLlJlc29sdmVkUmVmZXJlbmNlIiEKEUdldE1hcmtlcnNSZXF1ZXN0EgwKBFZYSUQYASABKAkiNQoSR2V0TWFya2Vyc1Jlc3BvbnNlEh8KB21hcmtlcnMYASADKAsyDi5hcGkudjEuTWFya2VyIkUKFFN1Ym1pdE1hcmtlcnNSZXF1ZXN0EgwKBFZYSUQYASABKAkSHwoHbWFya2VycxgCIAMoCzIOLmFwaS52MS5NYXJrZXIqMQoOQm1tRW52aXJvbm1lbnQSDgoKUHJvZHVjdGlvbhAAEg8KC0ludGVncmF0aW9uEAEqugEKDUNhbnRlbW9BY3Rpb24SHgoaQ0FOVEVNT19BQ1RJT05fVU5TUEVDSUZJRUQQABIaChZDQU5URU1PX0FDVElPTl9QUkVWSUVXEAESHQoZQ0FOVEVNT19BQ1RJT05fVFJBTlNDUklCRRACEikKJUNBTlRFTU9fQUNUSU9OX1NVQlRJVExFX0ZST01fU1VCVFJBTlMQAxIjCh9DQU5URU1PX0FDVElPTl9VUERBVEVfUkVMQVRJT05TEAQqqQEKCk1hcmtlclR5cGUSGwoXTUFSS0VSX1RZUEVfVU5TUEVDSUZJRUQQABIaChZNQVJLRVJfVFlQRV9OQU1FX1NVUEVSEAESGwoXTUFSS0VSX1RZUEVfQklCTEVfVkVSU0UQAhIUChBNQVJLRVJfVFlQRV9TT05HEAMSFwoTTUFSS0VSX1RZUEVfQ0hBUFRFUhAEEhYKEk1BUktFUl9UWVBFX0NVU1RPTRAFKmMKDE1hcmtlclNvdXJjZRIdChlNQVJLRVJfU09VUkNFX1VOU1BFQ0lGSUVEEAASGgoWTUFSS0VSX1NPVVJDRV9JTVBPUlRFRBABEhgKFE1BUktFUl9TT1VSQ0VfTUFOVUFMEAIymBAKCkFQSVNlcnZpY2USNQoOR2V0UGVybWlzc2lvbnMSDC5hcGkudjEuVm9pZBoTLmFwaS52MS5QZXJtaXNzaW9ucyIAEkIKEVVwZGF0ZVBlcm1pc3Npb25zEh0uYXBpLnYxLlNldFBlcm1pc3Npb25zUmVxdWVzdBoMLmFwaS52MS5Wb2lkIgASRQoRRGVsZXRlUGVybWlzc2lvbnMSIC5hcGkudjEuRGVsZXRlUGVybWlzc2lvbnNSZXF1ZXN0GgwuYXBpLnYxLlZvaWQiABI6Cg9MaXN0UGVybWlzc2lvbnMSDC5hcGkudjEuVm9pZBoXLmFwaS52MS5QZXJtaXNzaW9uc0xpc3QiABJLChBHZXRUcmFuc2NyaXB0aW9uEh4uYXBpLnYxLkdldFRyYW5zY3JpcHRpb25SZXFlc3QaFS5hcGkudjEuVHJhbnNjcmlwdGlvbiIAEjoKCkdldFByZXZpZXcSGS5hcGkudjEuR2V0UHJldmlld1JlcXVlc3QaDy5hcGkudjEuUHJldmlldyIAEkkKE1N1Ym1pdFRyYW5zY3JpcHRpb24SIi5hcGkudjEuU3VibWl0VHJhbnNjcmlwdGlvblJlcXVlc3QaDC5hcGkudjEuVm9pZCIAEj8KCEdldFllYXJzEhcuYXBpLnYxLkdldFllYXJzUmVxdWVzdBoYLmFwaS52MS5HZXRZZWFyc1Jlc3BvbnNlIgASOwoJR2V0QWxidW1zEhguYXBpLnYxLkdldEFsYnVtc1JlcXVlc3QaEi5hcGkudjEuQWxidW1zTGlzdCIAEkUKDkdldEFsYnVtVHJhY2tzEh0uYXBpLnYxLkdldEFsYnVtVHJhY2tzUmVxdWVzdBoSLmFwaS52MS5UcmFja3NMaXN0IgASSQoQR2V0UG9kY2FzdFRyYWNrcxIfLmFwaS52MS5HZXRQb2RjYXN0VHJhY2tzUmVxdWVzdBoSLmFwaS52MS5UcmFja3NMaXN0IgASTAoMR2V0TGFuZ3VhZ2VzEiQuYXBpLnYxLkdldEF2YWlsYWJsZUxhbmd1YWdlc1JlcXVlc3QaFC5hcGkudjEuTGFuZ3VhZ2VMaXN0IgASUgoTR2V0Qk1NVHJhbnNjcmlwdGlvbhIiLmFwaS52MS5HZXRCTU1UcmFuc2NyaXB0aW9uUmVxdWVzdBoVLmFwaS52MS5UcmFuc2NyaXB0aW9uIgASOQoLU3VibWl0U2hvcnQSGi5hcGkudjEuU3VibWl0U2hvcnRSZXF1ZXN0GgwuYXBpLnYxLlZvaWQiABJUCg9HZXRFeHBvcnRDb25maWcSHi5hcGkudjEuR2V0RXhwb3J0Q29uZmlnUmVxdWVzdBofLmFwaS52MS5HZXRFeHBvcnRDb25maWdSZXNwb25zZSIAEkgKC1N0YXJ0RXhwb3J0EhouYXBpLnYxLlN0YXJ0RXhwb3J0UmVxdWVzdBobLmFwaS52MS5TdGFydEV4cG9ydFJlc3BvbnNlIgASSQoTRXhwb3J0VGltZWRNZXRhZGF0YRIiLmFwaS52MS5FeHBvcnRUaW1lZE1ldGFkYXRhUmVxdWVzdBoMLmFwaS52MS5Wb2lkIgASTgoNUmVzb2x2ZUFzc2V0cxIcLmFwaS52MS5SZXNvbHZlQXNzZXRzUmVxdWVzdBodLmFwaS52MS5SZXNvbHZlQXNzZXRzUmVzcG9uc2UiABJaChFHZXRWQkV4cG9ydENvbmZpZxIgLmFwaS52MS5HZXRWQkV4cG9ydENvbmZpZ1JlcXVlc3QaIS5hcGkudjEuR2V0VkJFeHBvcnRDb25maWdSZXNwb25zZSIAEk4KDVN0YXJ0VkJFeHBvcnQSHC5hcGkudjEuU3RhcnRWQkV4cG9ydFJlcXVlc3QaHS5hcGkudjEuU3RhcnRWQkV4cG9ydFJlc3BvbnNlIgASSwoVR2V0RXhwb3J0RGVzdGluYXRpb25zEgwuYXBpLnYxLlZvaWQaIi5hcGkudjEuRXhwb3J0RGVzdGluYXRpb25zUmVzcG9uc2UiABJLChRUcmlnZ2VyQ2FudGVtb0FjdGlvbhIjLmFwaS52MS5UcmlnZ2VyQ2FudGVtb0FjdGlvblJlcXVlc3QaDC5hcGkudjEuVm9pZCIAEkgKC1ZhdWx0U2VhcmNoEhouYXBpLnYxLlZhdWx0U2VhcmNoUmVxdWVzdBobLmFwaS52MS5WYXVsdFNlYXJjaFJlc3BvbnNlIgASSwoMR2V0VmF1bHRJdGVtEhsuYXBpLnYxLkdldFZhdWx0SXRlbVJlcXVlc3QaHC5hcGkudjEuR2V0VmF1bHRJdGVtUmVzcG9uc2UiABJFCgpHZXRNYXJrZXJzEhkuYXBpLnYxLkdldE1hcmtlcnNSZXF1ZXN0GhouYXBpLnYxLkdldE1hcmtlcnNSZXNwb25zZSIAEj0KDVN1Ym1pdE1hcmtlcnMSHC5hcGkudjEuU3VibWl0TWFya2Vyc1JlcXVlc3QaDC5hcGkudjEuVm9pZCIAElEKDlNlYXJjaEVudGl0aWVzEh0uYXBpLnYxLlNlYXJjaEVudGl0aWVzUmVxdWVzdBoeLmFwaS52MS5TZWFyY2hFbnRpdGllc1Jlc3BvbnNlIgASWgoRUmVzb2x2ZVJlZmVyZW5jZXMSIC5hcGkudjEuUmVzb2x2ZVJlZmVyZW5jZXNSZXF1ZXN0GiEuYXBpLnYxLlJlc29sdmVSZWZlcmVuY2VzUmVzcG9uc2UiAEIeWhxiY2MtbWVkaWEtdG9vbHMvYXBpL3YxO2FwaXYxYgZwcm90bzM", [file_google_protobuf_timestamp, file_api_v1_common]); /** * @generated from message api.v1.BMMPermission @@ -1584,6 +1584,317 @@ export type GetVaultItemResponse = Message<"api.v1.GetVaultItemResponse"> & { export const GetVaultItemResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_api_v1_api, 56); +/** + * @generated from message api.v1.Marker + */ +export type Marker = Message<"api.v1.Marker"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: api.v1.MarkerType type = 2; + */ + type: MarkerType; + + /** + * Display text: the name, the verse reference, the song/chapter title. + * Always present; for linked markers it mirrors the canonical entity label, + * for custom / free-text markers it's whatever the user typed. + * + * @generated from field: string label = 3; + */ + label: string; + + /** + * @generated from field: string note = 4; + */ + note: string; + + /** + * In/out points in seconds. + * + * @generated from field: double start = 5; + */ + start: number; + + /** + * @generated from field: double end = 6; + */ + end: number; + + /** + * @generated from field: api.v1.MarkerSource source = 7; + */ + source: MarkerSource; + + /** + * Canonical entity reference, when the label is linked to a known entity (a + * bible passage, a song, a person). Empty for free-text / custom markers. + * `label` is kept as denormalized display text so linking is enrichment, not + * a requirement — unmatched markers still round-trip. + * + * @generated from field: string entity_id = 8; + */ + entityId: string; + + /** + * Which registry `entity_id` belongs to: "bible" | "songbook" | "people". + * Empty when `entity_id` is empty. + * + * @generated from field: string entity_source = 9; + */ + entitySource: string; +}; + +/** + * Describes the message api.v1.Marker. + * Use `create(MarkerSchema)` to create a new message. + */ +export const MarkerSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 57); + +/** + * A resolvable entity behind a marker label, returned by SearchEntities for + * autocomplete. `id`/`source` map to Marker.entity_id/entity_source. + * + * @generated from message api.v1.Entity + */ +export type Entity = Message<"api.v1.Entity"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string source = 2; + */ + source: string; + + /** + * Canonical display text, e.g. "John 3:16". + * + * @generated from field: string label = 3; + */ + label: string; + + /** + * Optional secondary text shown under the label, e.g. a translation or book. + * + * @generated from field: string detail = 4; + */ + detail: string; +}; + +/** + * Describes the message api.v1.Entity. + * Use `create(EntitySchema)` to create a new message. + */ +export const EntitySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 58); + +/** + * @generated from message api.v1.SearchEntitiesRequest + */ +export type SearchEntitiesRequest = Message<"api.v1.SearchEntitiesRequest"> & { + /** + * Which kind of entity to search — mapped from the marker's type. + * + * @generated from field: api.v1.MarkerType type = 1; + */ + type: MarkerType; + + /** + * @generated from field: string query = 2; + */ + query: string; + + /** + * Max results to return; server clamps to a sane bound. 0 means default. + * + * @generated from field: int32 limit = 3; + */ + limit: number; +}; + +/** + * Describes the message api.v1.SearchEntitiesRequest. + * Use `create(SearchEntitiesRequestSchema)` to create a new message. + */ +export const SearchEntitiesRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 59); + +/** + * @generated from message api.v1.SearchEntitiesResponse + */ +export type SearchEntitiesResponse = Message<"api.v1.SearchEntitiesResponse"> & { + /** + * @generated from field: repeated api.v1.Entity entities = 1; + */ + entities: Entity[]; +}; + +/** + * Describes the message api.v1.SearchEntitiesResponse. + * Use `create(SearchEntitiesResponseSchema)` to create a new message. + */ +export const SearchEntitiesResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 60); + +/** + * One free-text label to try to resolve to a canonical entity, used by the bulk + * "resolve references" action. + * + * @generated from message api.v1.ReferenceQuery + */ +export type ReferenceQuery = Message<"api.v1.ReferenceQuery"> & { + /** + * Opaque id echoed back so the caller can correlate results (the marker id). + * + * @generated from field: string ref_id = 1; + */ + refId: string; + + /** + * @generated from field: api.v1.MarkerType type = 2; + */ + type: MarkerType; + + /** + * @generated from field: string text = 3; + */ + text: string; +}; + +/** + * Describes the message api.v1.ReferenceQuery. + * Use `create(ReferenceQuerySchema)` to create a new message. + */ +export const ReferenceQuerySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 61); + +/** + * @generated from message api.v1.ResolvedReference + */ +export type ResolvedReference = Message<"api.v1.ResolvedReference"> & { + /** + * @generated from field: string ref_id = 1; + */ + refId: string; + + /** + * True only when the text resolved to a single, confident match. + * + * @generated from field: bool resolved = 2; + */ + resolved: boolean; + + /** + * Set only when resolved. + * + * @generated from field: api.v1.Entity entity = 3; + */ + entity?: Entity | undefined; +}; + +/** + * Describes the message api.v1.ResolvedReference. + * Use `create(ResolvedReferenceSchema)` to create a new message. + */ +export const ResolvedReferenceSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 62); + +/** + * @generated from message api.v1.ResolveReferencesRequest + */ +export type ResolveReferencesRequest = Message<"api.v1.ResolveReferencesRequest"> & { + /** + * @generated from field: repeated api.v1.ReferenceQuery queries = 1; + */ + queries: ReferenceQuery[]; +}; + +/** + * Describes the message api.v1.ResolveReferencesRequest. + * Use `create(ResolveReferencesRequestSchema)` to create a new message. + */ +export const ResolveReferencesRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 63); + +/** + * @generated from message api.v1.ResolveReferencesResponse + */ +export type ResolveReferencesResponse = Message<"api.v1.ResolveReferencesResponse"> & { + /** + * @generated from field: repeated api.v1.ResolvedReference results = 1; + */ + results: ResolvedReference[]; +}; + +/** + * Describes the message api.v1.ResolveReferencesResponse. + * Use `create(ResolveReferencesResponseSchema)` to create a new message. + */ +export const ResolveReferencesResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 64); + +/** + * @generated from message api.v1.GetMarkersRequest + */ +export type GetMarkersRequest = Message<"api.v1.GetMarkersRequest"> & { + /** + * @generated from field: string VXID = 1; + */ + VXID: string; +}; + +/** + * Describes the message api.v1.GetMarkersRequest. + * Use `create(GetMarkersRequestSchema)` to create a new message. + */ +export const GetMarkersRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 65); + +/** + * @generated from message api.v1.GetMarkersResponse + */ +export type GetMarkersResponse = Message<"api.v1.GetMarkersResponse"> & { + /** + * @generated from field: repeated api.v1.Marker markers = 1; + */ + markers: Marker[]; +}; + +/** + * Describes the message api.v1.GetMarkersResponse. + * Use `create(GetMarkersResponseSchema)` to create a new message. + */ +export const GetMarkersResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 66); + +/** + * @generated from message api.v1.SubmitMarkersRequest + */ +export type SubmitMarkersRequest = Message<"api.v1.SubmitMarkersRequest"> & { + /** + * @generated from field: string VXID = 1; + */ + VXID: string; + + /** + * @generated from field: repeated api.v1.Marker markers = 2; + */ + markers: Marker[]; +}; + +/** + * Describes the message api.v1.SubmitMarkersRequest. + * Use `create(SubmitMarkersRequestSchema)` to create a new message. + */ +export const SubmitMarkersRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_api, 67); + /** * @generated from enum api.v1.BmmEnvironment */ @@ -1641,6 +1952,77 @@ export enum CantemoAction { export const CantemoActionSchema: GenEnum = /*@__PURE__*/ enumDesc(file_api_v1_api, 1); +/** + * @generated from enum api.v1.MarkerType + */ +export enum MarkerType { + /** + * @generated from enum value: MARKER_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: MARKER_TYPE_NAME_SUPER = 1; + */ + NAME_SUPER = 1, + + /** + * @generated from enum value: MARKER_TYPE_BIBLE_VERSE = 2; + */ + BIBLE_VERSE = 2, + + /** + * @generated from enum value: MARKER_TYPE_SONG = 3; + */ + SONG = 3, + + /** + * @generated from enum value: MARKER_TYPE_CHAPTER = 4; + */ + CHAPTER = 4, + + /** + * @generated from enum value: MARKER_TYPE_CUSTOM = 5; + */ + CUSTOM = 5, +} + +/** + * Describes the enum api.v1.MarkerType. + */ +export const MarkerTypeSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_api_v1_api, 2); + +/** + * Where a marker originated. IMPORTED markers come from the third-party timing + * program (name-supers, bible-verse references, …); MANUAL ones are created in + * this tool. The source is preserved through edits for reconciliation. + * + * @generated from enum api.v1.MarkerSource + */ +export enum MarkerSource { + /** + * @generated from enum value: MARKER_SOURCE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: MARKER_SOURCE_IMPORTED = 1; + */ + IMPORTED = 1, + + /** + * @generated from enum value: MARKER_SOURCE_MANUAL = 2; + */ + MANUAL = 2, +} + +/** + * Describes the enum api.v1.MarkerSource. + */ +export const MarkerSourceSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_api_v1_api, 3); + /** * @generated from service api.v1.APIService */ @@ -1857,6 +2239,44 @@ export const APIService: GenService<{ input: typeof GetVaultItemRequestSchema; output: typeof GetVaultItemResponseSchema; }, + /** + * Markers + * + * @generated from rpc api.v1.APIService.GetMarkers + */ + getMarkers: { + methodKind: "unary"; + input: typeof GetMarkersRequestSchema; + output: typeof GetMarkersResponseSchema; + }, + /** + * @generated from rpc api.v1.APIService.SubmitMarkers + */ + submitMarkers: { + methodKind: "unary"; + input: typeof SubmitMarkersRequestSchema; + output: typeof VoidSchema; + }, + /** + * Autocomplete for marker labels (bible passages, songs, people). + * + * @generated from rpc api.v1.APIService.SearchEntities + */ + searchEntities: { + methodKind: "unary"; + input: typeof SearchEntitiesRequestSchema; + output: typeof SearchEntitiesResponseSchema; + }, + /** + * Bulk-resolve free-text marker labels to canonical entities. + * + * @generated from rpc api.v1.APIService.ResolveReferences + */ + resolveReferences: { + methodKind: "unary"; + input: typeof ResolveReferencesRequestSchema; + output: typeof ResolveReferencesResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_api_v1_api, 0);
+ + {{ t("markers.review.hint") }} +
+ {{ t("markers.loadError") }} +
+ {{ t("markers.empty.title") }} +
+ {{ t("markers.empty.hint") }} +
+ {{ t("markers.index.description") }} +
+ {{ t("markers.index.demoHint") }} +