Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ module.exports = function(grunt) {
},
dist_admin: {
src: [
'public/javascripts/common/AiLabelIndicator.js',
'public/javascripts/Admin/src/util/*.js',
'public/javascripts/Admin/src/*.js',
'public/javascripts/common/UtilitiesSidewalk.js',
'public/javascripts/common/Panomarker.js',
Expand All @@ -62,6 +64,7 @@ module.exports = function(grunt) {
},
dist_validate: {
src: [
'public/javascripts/common/AiLabelIndicator.js',
'public/javascripts/SVValidate/src/*.js',
'public/javascripts/SVValidate/src/data/*.js',
'public/javascripts/SVValidate/src/keyboard/*.js',
Expand All @@ -84,6 +87,7 @@ module.exports = function(grunt) {
},
dist_gallery: {
src: [
'public/javascripts/common/AiLabelIndicator.js',
'public/javascripts/Gallery/src/cards/*.js',
'public/javascripts/Gallery/src/data/*.js',
'public/javascripts/Gallery/src/filter/*.js',
Expand Down
6 changes: 4 additions & 2 deletions app/controllers/AdminController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ class AdminController @Inject() (
"label_type" -> label.labelType,
"severity" -> label.severity,
"correct" -> label.correct,
"high_quality_user" -> label.highQualityUser
"high_quality_user" -> label.highQualityUser,
"ai_generated" -> label.aiGenerated
)
)
}.seq
Expand Down Expand Up @@ -187,7 +188,8 @@ class AdminController @Inject() (
"has_validations" -> label.hasValidations,
"ai_validation" -> label.aiValidation.map(LabelValidationTable.validationOptions.get),
"expired" -> label.expired,
"high_quality_user" -> label.highQualityUser
"high_quality_user" -> label.highQualityUser,
"ai_generated" -> label.aiGenerated
)
)
}.seq
Expand Down
7 changes: 5 additions & 2 deletions app/formats/json/LabelFormats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ object LabelFormats {
(__ \ "validations").write[Map[String, Int]] and
(__ \ "tags").write[List[String]] and
(__ \ "low_quality_incomplete_stale_flags").write[(Boolean, Boolean, Boolean)] and
(__ \ "comments").write[Option[Seq[String]]]
(__ \ "comments").write[Option[Seq[String]]] and
(__ \ "ai_generated").write[Boolean]
)(unlift(LabelMetadata.unapply))

def validationLabelMetadataToJson(
Expand Down Expand Up @@ -95,6 +96,7 @@ object LabelFormats {
"ai_validation" -> labelMetadata.aiValidation.map(LabelValidationTable.validationOptions.get),
"tags" -> labelMetadata.tags,
"ai_tags" -> labelMetadata.aiTags,
"ai_generated" -> labelMetadata.aiGenerated,
"admin_data" -> adminData.map(ad =>
Json.obj(
"username" -> ad.username,
Expand Down Expand Up @@ -133,7 +135,8 @@ object LabelFormats {
"num_disagree" -> labelMetadata.validations("disagree"),
"num_unsure" -> labelMetadata.validations("unsure"),
"comments" -> labelMetadata.comments,
"tags" -> labelMetadata.tags
"tags" -> labelMetadata.tags,
"ai_generated" -> labelMetadata.aiGenerated
)
}

Expand Down
158 changes: 100 additions & 58 deletions app/models/label/LabelTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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
Expand All @@ -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
)
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
)
}

Expand Down Expand Up @@ -743,7 +750,13 @@ class LabelTable @Inject() (
at.low_quality,
at.incomplete,
at.stale,
comment.comments
comment.comments,
EXISTS (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that, instead of adding a subquery here, it would be more efficient to just do the joins in the main query. So within the query it would look like

...
INNER JOIN sidewalk_user AS u ON at.user_id = u.user_id
INNER JOIN user_role AS ur ON u.user_id = ur.user_id
INNER JOIN role AS r ON ur.role_id = r.role_id
INNER JOIN label_point AS lp ON lb1.label_id = lp.label_id
...

And then you could just refer to it as r.role = 'AI' rather than doing the subquery in the SELECT statement.

This comment was marked as resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@misaugstad I removed the per-row EXISTS subquery in getRecentLabelsMetadata and pulled the role check into the main query: we now join user_role/role once and select (ai_user.user_id IS NOT NULL) AS ai_generated, using r.role = 'AI' from the join instead of a subquery.

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
Expand Down Expand Up @@ -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)
.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
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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
)
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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))
)
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading