-
Notifications
You must be signed in to change notification settings - Fork 27
Added AI-generated label indicator and tooltip message #4094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 4 commits
015b2e2
dfd5984
a779a63
2d1f280
b160137
fc2baba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,7 +76,8 @@ case class LabelForLabelMap( | |
| aiValidation: Option[Int], | ||
| expired: Boolean, | ||
| highQualityUser: Boolean, | ||
| severity: Option[Int] | ||
| severity: Option[Int], | ||
| aiGenerated: Boolean | ||
| ) | ||
|
|
||
| case class TagCount(labelType: String, tag: String, count: Int) | ||
|
|
@@ -144,7 +145,8 @@ case class LabelMetadata( | |
| validations: Map[String, Int], | ||
| tags: List[String], | ||
| lowQualityIncompleteStaleFlags: (Boolean, Boolean, Boolean), | ||
| comments: Option[Seq[String]] | ||
| comments: Option[Seq[String]], | ||
| aiGenerated: Boolean | ||
| ) | ||
|
|
||
| // Extra data to include with validations for Admin Validate. Includes usernames and previous validators. | ||
|
|
@@ -226,7 +228,8 @@ case class LabelValidationMetadata( | |
| tags: Seq[String], | ||
| cameraLat: Option[Float], | ||
| cameraLng: Option[Float], | ||
| aiTags: Option[Seq[String]] | ||
| aiTags: Option[Seq[String]], | ||
| aiGenerated: Boolean | ||
| ) extends BasicLabelMetadata | ||
|
|
||
| class LabelTableDef(tag: slick.lifted.Tag) extends Table[Label](tag, "label") { | ||
|
|
@@ -321,7 +324,8 @@ object LabelTable { | |
| List[String], // tags | ||
| Option[Float], // cameraLat | ||
| Option[Float], // cameraLng | ||
| Option[List[String]] // aiTags | ||
| Option[List[String]], // aiTags | ||
| Boolean // aiGenerated | ||
| ) | ||
| type LabelValidationMetadataTupleRep = ( | ||
| Rep[Int], // labelId | ||
|
|
@@ -343,15 +347,16 @@ object LabelTable { | |
| Rep[List[String]], // tags | ||
| Rep[Option[Float]], // cameraLat | ||
| Rep[Option[Float]], // cameraLng | ||
| Rep[Option[List[String]]] // aiTags | ||
| Rep[Option[List[String]]], // aiTags | ||
| Rep[Boolean] // aiGenerated | ||
| ) | ||
|
|
||
| // Define an implicit conversion from the tuple representation to the case class. | ||
| implicit val labelValidationMetadataConverter: TupleConverter[LabelValidationMetadataTuple, LabelValidationMetadata] = | ||
| new TupleConverter[LabelValidationMetadataTuple, LabelValidationMetadata] { | ||
| def fromTuple(t: LabelValidationMetadataTuple): LabelValidationMetadata = LabelValidationMetadata( | ||
| t._1, t._2, t._3, t._4, t._5, t._6.get, t._7.get, POV.tupled(t._8), LocationXY.tupled(t._9), t._10, t._11, | ||
| t._12, t._13, LabelValidationInfo.tupled(t._14), t._15, t._16, t._17, t._18, t._19, t._20 | ||
| t._12, t._13, LabelValidationInfo.tupled(t._14), t._15, t._16, t._17, t._18, t._19, t._20, t._21 | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -486,8 +491,9 @@ class LabelTable @Inject() ( | |
|
|
||
| val aiValidations = | ||
| labelValidations.join(sidewalkUserTable.aiUsers).on(_.userId === _.userId).map(_._1).distinctOn(_.labelId) | ||
| val neighborhoods = regions.filter(_.deleted === false) | ||
| val usersWithoutExcluded = usersUnfiltered | ||
| private val aiRoles = userRoles.join(roleTable).on(_.roleId === _.roleId).filter(_._2.role === "AI") | ||
| val neighborhoods = regions.filter(_.deleted === false) | ||
| val usersWithoutExcluded = usersUnfiltered | ||
| .join(userStats) | ||
| .on(_.userId === _.userId) | ||
| .filterNot(_._2.excluded) // Exclude users with excluded = true | ||
|
|
@@ -552,7 +558,8 @@ class LabelTable @Inject() ( | |
| r.nextString().split(',').map(x => x.split(':')).map { y => (y(0), y(1).toInt) }.toMap, | ||
| r.nextString().split(",").filter(_.nonEmpty).toList, | ||
| (r.nextBoolean(), r.nextBoolean(), r.nextBoolean()), | ||
| r.nextStringOption().filter(_.nonEmpty).map(_.split(":").filter(_.nonEmpty).toSeq) | ||
| r.nextStringOption().filter(_.nonEmpty).map(_.split(":").filter(_.nonEmpty).toSeq), | ||
| r.nextBoolean() | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -743,7 +750,13 @@ class LabelTable @Inject() ( | |
| at.low_quality, | ||
| at.incomplete, | ||
| at.stale, | ||
| comment.comments | ||
| comment.comments, | ||
| EXISTS ( | ||
|
||
| SELECT 1 | ||
| FROM user_role ur | ||
| INNER JOIN role r ON ur.role_id = r.role_id | ||
| WHERE ur.user_id = lb1.user_id AND r.role = 'AI' | ||
| ) AS ai_generated | ||
| FROM label AS lb1 | ||
| INNER JOIN gsv_data ON lb1.gsv_panorama_id = gsv_data.gsv_panorama_id | ||
| INNER JOIN audit_task AS at ON lb1.audit_task_id = at.audit_task_id | ||
|
|
@@ -859,17 +872,27 @@ class LabelTable @Inject() ( | |
| if userIds.map(ids => _lb.userId inSetBind ids).getOrElse(true: Rep[Boolean]) // Filter by user IDs. | ||
| } yield (_lb, _lp, _lt, _gd, _us, _ser, _at) | ||
|
|
||
| // Attach AI role info via left join to avoid per-row subqueries. | ||
| val _labelInfoWithAiRole = _labelInfo | ||
| .joinLeft(aiRoles) | ||
misaugstad marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .on { case ((lb, _, _, _, _, _, _), aiRole) => lb.userId === aiRole._1.userId } | ||
| .map { case ((lb, lp, lt, gd, us, ser, at), aiRole) => (lb, lp, lt, gd, us, ser, at, aiRole) } | ||
|
|
||
| // Get AI validations. | ||
| val _labelInfoWithAIValidation = _labelInfo | ||
| val _labelInfoWithAIValidation = _labelInfoWithAiRole | ||
| .joinLeft(aiValidations) | ||
| .on(_._1.labelId === _.labelId) | ||
| .map { case ((_lb, _lp, _lt, _gd, _us, _ser, _at), _aiv) => (_lb, _lp, _lt, _gd, _us, _ser, _at, _aiv) } | ||
| .map { case ((_lb, _lp, _lt, _gd, _us, _ser, _at, _aiRole), _aiv) => | ||
| (_lb, _lp, _lt, _gd, _us, _ser, _at, _aiRole, _aiv) | ||
| } | ||
|
|
||
| // Get AI suggested tags. | ||
| val _labelInfoWithAiTagSuggestions = _labelInfoWithAIValidation | ||
| .joinLeft(labelAiAssessments) | ||
| .on(_._1.labelId === _.labelId) | ||
| .map { case ((_lb, _lp, _lt, _gd, _us, _ser, _at, _aiv), _la) => (_lb, _lp, _lt, _gd, _us, _ser, _at, _aiv, _la) } | ||
| .map { case ((_lb, _lp, _lt, _gd, _us, _ser, _at, _aiRole, _aiv), _la) => | ||
| (_lb, _lp, _lt, _gd, _us, _ser, _at, _aiRole, _aiv, _la) | ||
| } | ||
|
|
||
| // Filter out labels that have already been validated by this user. | ||
| val _labelInfoFiltered = _labelInfoWithAiTagSuggestions | ||
|
|
@@ -881,7 +904,7 @@ class LabelTable @Inject() ( | |
| // Priority ordering algorithm is described in the method comment, max score is 276. | ||
| val _labelInfoSorted = _labelInfoFiltered | ||
| .sortBy { | ||
| case (l, lp, lt, gd, us, ser, at, aiv, la) => { | ||
| case (l, lp, lt, gd, us, ser, at, aiRole, aiv, la) => { | ||
| // A label gets 150 if the labeler as < 50 of their labels validated (and this label needs a validation). | ||
| val needsValidationScore = | ||
| Case.If(us.ownLabelsValidated < 50 && l.correct.isEmpty && !at.lowQuality && !at.stale).Then(150d).Else(0d) | ||
|
|
@@ -907,7 +930,7 @@ class LabelTable @Inject() ( | |
| } | ||
| } | ||
| // Select only the columns needed for the LabelValidationMetadata class. | ||
| .map { case (l, lp, lt, gd, us, ser, at, aiv, la) => | ||
| .map { case (l, lp, lt, gd, us, ser, at, aiRole, aiv, la) => | ||
| ( | ||
| l.labelId, | ||
| lt.labelType, | ||
|
|
@@ -930,7 +953,8 @@ class LabelTable @Inject() ( | |
| gd.lng, | ||
| // Include AI tags if requested. | ||
| if (includeAiTags) la.flatMap(_.tags).getOrElse(List.empty[String].bind).asColumnOf[Option[List[String]]] | ||
| else None.asInstanceOf[Option[List[String]]].asColumnOf[Option[List[String]]] | ||
| else None.asInstanceOf[Option[List[String]]].asColumnOf[Option[List[String]]], | ||
| aiRole.isDefined | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -1015,57 +1039,67 @@ class LabelTable @Inject() ( | |
| if (_lb.tags @& tags.toList) || tags.isEmpty // @& is the overlap operator from postgres (&& in postgres). | ||
| if _us.highQuality || (_lb.correct.isDefined && _lb.correct === true) | ||
| if _lb.disagreeCount < 3 || _lb.disagreeCount < _lb.agreeCount * 2 | ||
| } yield (_lb, _lp, _lt, _gd, _ser) | ||
| } yield (_lb, _lp, _lt, _gd, _ser, _us) | ||
|
|
||
| val _labelInfoWithAiRole = _labelInfo | ||
| .joinLeft(aiRoles) | ||
| .on { case ((lb, _, _, _, _, _), aiRole) => lb.userId === aiRole._1.userId } | ||
| .map { case ((lb, lp, lt, gd, ser, us), aiRole) => (lb, lp, lt, gd, ser, us, aiRole) } | ||
|
|
||
| // Get AI validations. | ||
| val _labelInfoWithAIValidation = _labelInfo | ||
| val _labelInfoWithAIValidation = _labelInfoWithAiRole | ||
| .joinLeft(aiValidations) | ||
| .on(_._1.labelId === _.labelId) | ||
| .map { case ((_lb, _lp, _lt, _gd, _ser), _aiv) => (_lb, _lp, _lt, _gd, _ser, _aiv) } | ||
| .map { case ((_lb, _lp, _lt, _gd, _ser, _us, _aiRole), _aiv) => | ||
| (_lb, _lp, _lt, _gd, _ser, _us, _aiRole, _aiv) | ||
| } | ||
|
|
||
| // Filter labels based on how the AI validated them. If no filters provided, do no filtering here. | ||
| val _labelsFilteredByAiValidation = { | ||
| var query = _labelInfoWithAIValidation | ||
| if (aiValOptions.nonEmpty) { | ||
| if (!aiValOptions.contains("correct")) | ||
| query = query.filter(l => l._6.isEmpty || l._6.map(_.validationResult) =!= 1.asColumnOf[Option[Int]]) | ||
| query = query.filter(l => l._8.isEmpty || l._8.map(_.validationResult) =!= 1.asColumnOf[Option[Int]]) | ||
| if (!aiValOptions.contains("incorrect")) | ||
| query = query.filter(l => l._6.isEmpty || l._6.map(_.validationResult) =!= 2.asColumnOf[Option[Int]]) | ||
| query = query.filter(l => l._8.isEmpty || l._8.map(_.validationResult) =!= 2.asColumnOf[Option[Int]]) | ||
| if (!aiValOptions.contains("unsure")) | ||
| query = query.filter(l => l._6.isEmpty || l._6.map(_.validationResult) =!= 3.asColumnOf[Option[Int]]) | ||
| if (!aiValOptions.contains("unvalidated")) query = query.filter(l => l._6.isDefined) | ||
| query = query.filter(l => l._8.isEmpty || l._8.map(_.validationResult) =!= 3.asColumnOf[Option[Int]]) | ||
| if (!aiValOptions.contains("unvalidated")) query = query.filter(l => l._8.isDefined) | ||
| } | ||
| query | ||
| } | ||
|
|
||
| // Join with user validations. | ||
| val _userValidations = labelValidations.filter(_.userId === userId) | ||
| val _labelInfoWithUserVals = for { | ||
| ((_lb, _lp, _lt, _gd, _ser, _aiv), _uv) <- | ||
| _labelsFilteredByAiValidation.joinLeft(_userValidations).on(_._1.labelId === _.labelId) | ||
| } yield ( | ||
| _lb.labelId, | ||
| _lt.labelType, | ||
| _lb.gsvPanoramaId, | ||
| _gd.captureDate, | ||
| _lb.timeCreated, | ||
| _lp.lat, | ||
| _lp.lng, | ||
| (_lp.heading.asColumnOf[Double], _lp.pitch.asColumnOf[Double], _lp.zoom), | ||
| (_lp.canvasX, _lp.canvasY), | ||
| _lb.severity, | ||
| _lb.description, | ||
| _lb.streetEdgeId, | ||
| _ser.regionId, | ||
| (_lb.agreeCount, _lb.disagreeCount, _lb.unsureCount, _lb.correct), | ||
| _uv.map(_.validationResult), // userValidation | ||
| _aiv.map(_.validationResult), // aiValidation | ||
| _lb.tags, | ||
| _gd.lat, | ||
| _gd.lng, | ||
| // Placeholder for AI tags, since we don't show those on Gallery right now. | ||
| None.asInstanceOf[Option[List[String]]].asColumnOf[Option[List[String]]] | ||
| ) | ||
| val _labelInfoWithUserVals = _labelsFilteredByAiValidation | ||
| .joinLeft(_userValidations) | ||
| .on(_._1.labelId === _.labelId) | ||
| .map { case ((_lb, _lp, _lt, _gd, _ser, _us, _aiRole, _aiv), _uv) => | ||
| ( | ||
| _lb.labelId, | ||
| _lt.labelType, | ||
| _lb.gsvPanoramaId, | ||
| _gd.captureDate, | ||
| _lb.timeCreated, | ||
| _lp.lat, | ||
| _lp.lng, | ||
| (_lp.heading.asColumnOf[Double], _lp.pitch.asColumnOf[Double], _lp.zoom), | ||
| (_lp.canvasX, _lp.canvasY), | ||
| _lb.severity, | ||
| _lb.description, | ||
| _lb.streetEdgeId, | ||
| _ser.regionId, | ||
| (_lb.agreeCount, _lb.disagreeCount, _lb.unsureCount, _lb.correct), | ||
| _uv.map(_.validationResult), // userValidation | ||
| _aiv.map(_.validationResult), // aiValidation | ||
| _lb.tags, | ||
| _gd.lat, | ||
| _gd.lng, | ||
| // Placeholder for AI tags, since we don't show those on Gallery right now. | ||
| None.asInstanceOf[Option[List[String]]].asColumnOf[Option[List[String]]], | ||
| _aiRole.isDefined | ||
| ) | ||
| } | ||
|
|
||
| // Remove duplicates if needed and randomize. | ||
| val rand = SimpleFunction.nullary[Double]("random") | ||
|
|
@@ -1145,30 +1179,38 @@ class LabelTable @Inject() ( | |
| if _lp.lat.isDefined && _lp.lng.isDefined // Make sure they are NOT NULL so we can safely use .get later. | ||
| } yield (_l, _us, _lt, _lp, _gsv, _ser) | ||
|
|
||
| val _labelsWithAiRole = _labels | ||
| .joinLeft(aiRoles) | ||
| .on { case ((_l, _, _, _, _, _), _aiRole) => _l.userId === _aiRole._1.userId } | ||
| .map { case ((_l, _us, _lt, _lp, _gsv, _ser), _aiRole) => (_l, _us, _lt, _lp, _gsv, _ser, _aiRole) } | ||
|
|
||
| // Get AI validations. | ||
| val _labelInfoWithAIValidation = _labels | ||
| val _labelInfoWithAIValidation = _labelsWithAiRole | ||
| .joinLeft(aiValidations) | ||
| .on(_._1.labelId === _.labelId) | ||
| .map { case ((_l, _us, _lt, _lp, _gsv, _ser), _aiv) => (_l, _us, _lt, _lp, _gsv, _ser, _aiv) } | ||
| .map { case ((_l, _us, _lt, _lp, _gsv, _ser, _aiRole), _aiv) => | ||
| (_l, _us, _lt, _lp, _gsv, _ser, _aiRole, _aiv) | ||
| } | ||
|
|
||
| // Filter labels based on how the AI validated them. If no filters provided, do no filtering here. | ||
| val _labelsFilteredByAiValidation = { | ||
| var query = _labelInfoWithAIValidation | ||
| if (aiValOptions.nonEmpty) { | ||
| if (!aiValOptions.contains("correct")) | ||
| query = query.filter(l => l._7.isEmpty || l._7.map(_.validationResult) =!= 1.asColumnOf[Option[Int]]) | ||
| query = query.filter(l => l._8.isEmpty || l._8.map(_.validationResult) =!= 1.asColumnOf[Option[Int]]) | ||
| if (!aiValOptions.contains("incorrect")) | ||
| query = query.filter(l => l._7.isEmpty || l._7.map(_.validationResult) =!= 2.asColumnOf[Option[Int]]) | ||
| query = query.filter(l => l._8.isEmpty || l._8.map(_.validationResult) =!= 2.asColumnOf[Option[Int]]) | ||
| if (!aiValOptions.contains("unsure")) | ||
| query = query.filter(l => l._7.isEmpty || l._7.map(_.validationResult) =!= 3.asColumnOf[Option[Int]]) | ||
| if (!aiValOptions.contains("unvalidated")) query = query.filter(l => l._7.isDefined) | ||
| query = query.filter(l => l._8.isEmpty || l._8.map(_.validationResult) =!= 3.asColumnOf[Option[Int]]) | ||
| if (!aiValOptions.contains("unvalidated")) query = query.filter(l => l._8.isDefined) | ||
| } | ||
|
|
||
| // Grab the columns that we need for the LabelForLabelMap case class. | ||
| query.map { case (_l, _us, _lt, _lp, _gsv, _ser, _aiv) => | ||
| query.map { case (_l, _us, _lt, _lp, _gsv, _ser, _aiRole, _aiv) => | ||
| val hasValidations = _l.agreeCount > 0 || _l.disagreeCount > 0 || _l.unsureCount > 0 | ||
| (_l.labelId, _l.auditTaskId, _lt.labelType, _lp.lat, _lp.lng, _l.correct, hasValidations, | ||
| _aiv.map(_.validationResult), _gsv.expired, _us.highQuality, _l.severity, _ser.streetEdgeId) | ||
| _aiv.map(_.validationResult), _gsv.expired, _us.highQuality, _l.severity, _ser.streetEdgeId, | ||
| _aiRole.isDefined) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1189,7 +1231,7 @@ class LabelTable @Inject() ( | |
| // error, which is why we couldn't use `.tupled` here. This was the error message: | ||
| // SlickException: Expected an option type, found Float/REAL | ||
| _labelsNearRoute.result.map( | ||
| _.map(l => LabelForLabelMap(l._1, l._2, l._3, l._4.get, l._5.get, l._6, l._7, l._8, l._9, l._10, l._11)) | ||
| _.map(l => LabelForLabelMap(l._1, l._2, l._3, l._4.get, l._5.get, l._6, l._7, l._8, l._9, l._10, l._11, l._13)) | ||
| ) | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.