Compare commits

...
Author SHA1 Message Date
Brandon Thomas 3bbaa6b3a0 further cleanup 2026-07-04 11:42:44 -04:00
7 changed files with 62 additions and 1017 deletions
@@ -1,472 +0,0 @@
use std::collections::HashSet;
use chrono::{DateTime, Utc};
use sqlx::{FromRow, MySql, MySqlPool, QueryBuilder, Row};
use sqlx::mysql::MySqlRow;
use enums::by_table::model_weights::{
weights_category::WeightsCategory,
weights_types::WeightsType,
};
use enums::common::view_as::ViewAs;
use enums::traits::mysql_from_row::MySqlFromRow as _;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::users::UserToken;
pub struct FeaturedWeightsPage {
pub records: Vec<FeaturedWeight>,
pub sort_ascending: bool,
/// ID of the first record in the result set.
pub first_id: Option<i64>,
/// ID of the last record in the result set.
pub last_id: Option<i64>,
}
#[derive(Serialize)]
pub struct FeaturedWeight {
pub token: ModelWeightToken,
pub weights_type: WeightsType,
pub weights_category: WeightsCategory,
pub title: String,
// TODO(bt,2023-12-24): These aren't really appropriate for a list endpoint.
// Hopefully we don't break the frontend by omitting these.
//pub maybe_description_markdown: String,
//pub maybe_description_rendered_html: String,
pub maybe_ietf_language_tag: Option<String>,
pub maybe_ietf_primary_language_subtag: Option<String>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_username: Option<String>,
pub maybe_creator_display_name: Option<String>,
pub maybe_creator_email_gravatar_hash: Option<String>,
pub creator_ip_address: String,
pub creator_set_visibility: Visibility,
pub maybe_last_update_user_token: Option<UserToken>,
pub original_download_url: Option<String>,
pub original_filename: Option<String>,
pub file_size_bytes: i32,
pub file_checksum_sha2: String,
pub public_bucket_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_cover_image_public_bucket_hash: Option<String>,
pub maybe_cover_image_public_bucket_prefix: Option<String>,
pub maybe_cover_image_public_bucket_extension: Option<String>,
pub maybe_ratings_positive_count: Option<u32>,
pub maybe_ratings_negative_count: Option<u32>,
pub maybe_bookmark_count: Option<u32>,
pub cached_usage_count: u64,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub user_deleted_at: Option<DateTime<Utc>>,
pub mod_deleted_at: Option<DateTime<Utc>>,
}
pub struct ListFeaturedWeightsArgs<'a> {
pub limit: usize,
pub maybe_offset: Option<usize>,
pub cursor_is_reversed: bool,
pub sort_ascending: bool,
pub view_as: ViewAs,
pub maybe_scoped_weight_types: Option<&'a HashSet<WeightsType>>,
pub maybe_scoped_weight_categories: Option<&'a HashSet<WeightsCategory>>,
pub mysql_pool: &'a MySqlPool,
}
pub async fn list_featured_weights(args: ListFeaturedWeightsArgs<'_>) -> AnyhowResult<FeaturedWeightsPage> {
// TODO(bt,2024-05-05): Why were we doing this, again?
// let count_fields = select_total_count_field();
// let mut count_query_builder = query_builder(
// args.limit,
// args.maybe_offset,
// args.cursor_is_reversed,
// args.sort_ascending,
// count_fields.as_str(),
// args.view_as,
// args.maybe_scoped_weight_type,
// args.maybe_scoped_weight_category,
// );
// let row_count_query = count_query_builder.build_query_scalar::<i64>();
// let row_count_result = row_count_query.fetch_one(args.mysql_pool).await?;
/// Now fetch the actual results with all the fields
let result_fields = select_result_fields();
let mut query = query_builder(
args.limit,
args.maybe_offset,
args.cursor_is_reversed,
args.sort_ascending,
result_fields.as_str(),
args.view_as,
args.maybe_scoped_weight_types,
args.maybe_scoped_weight_categories,
);
let query = query.build_query_as::<RawWeightJoinUser>();
let results = query.fetch_all(args.mysql_pool).await?;
let first_id = results.first()
.map(|raw_result| raw_result.id);
let last_id = results.last()
.map(|raw_result| raw_result.id);
let weights_records: Vec<FeaturedWeight> = map_to_weights(results).await;
Ok(FeaturedWeightsPage {
records: weights_records,
sort_ascending: args.sort_ascending,
first_id,
last_id,
})
}
fn select_total_count_field() -> String {
r#"
COUNT(mw.id) AS total_count
"#
.to_string()
}
fn select_result_fields() -> String {
r#"
mw.id,
mw.token,
mw.title,
mw.weights_type,
mw.weights_category,
mw.maybe_ietf_language_tag,
mw.maybe_ietf_primary_language_subtag,
u.token as maybe_creator_user_token,
u.username as maybe_creator_username,
u.display_name as maybe_creator_display_name,
u.email_gravatar_hash as maybe_creator_email_gravatar_hash,
mw.creator_ip_address,
mw.creator_set_visibility,
mw.maybe_last_update_user_token,
mw.original_download_url,
mw.original_filename,
mw.file_size_bytes,
mw.file_checksum_sha2,
mw.public_bucket_hash,
mw.maybe_public_bucket_prefix,
mw.maybe_public_bucket_extension,
cover_image.public_bucket_directory_hash as maybe_cover_image_public_bucket_hash,
cover_image.maybe_public_bucket_prefix as maybe_cover_image_public_bucket_prefix,
cover_image.maybe_public_bucket_extension as maybe_cover_image_public_bucket_extension,
entity_stats.ratings_positive_count as maybe_ratings_positive_count,
entity_stats.ratings_negative_count as maybe_ratings_negative_count,
entity_stats.bookmark_count as maybe_bookmark_count,
mw.cached_usage_count,
mw.version,
mw.created_at,
mw.updated_at,
mw.user_deleted_at,
mw.mod_deleted_at
"#.to_string()
}
fn query_builder<'a>(
_limit: usize, // TODO(bt,2024-05-16): This seems wrong. Why is this unused?
maybe_offset: Option<usize>,
cursor_is_reversed: bool,
sort_ascending: bool,
select_fields: &'a str,
view_as: ViewAs,
maybe_scoped_weight_types: Option<&'a HashSet<WeightsType>>,
maybe_scoped_weight_categories: Option<&'a HashSet<WeightsCategory>>,
) -> QueryBuilder<'a, MySql> {
// NB: Query cannot be statically checked by sqlx
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
format!(r#"
SELECT
{select_fields}
FROM model_weights as mw
JOIN featured_items
ON featured_items.entity_type = "model_weight"
AND featured_items.deleted_at IS NULL
AND featured_items.entity_token = mw.token
LEFT OUTER JOIN users as u
ON u.token = mw.creator_user_token
LEFT OUTER JOIN media_files as cover_image
ON cover_image.token = mw.maybe_cover_image_media_file_token
LEFT OUTER JOIN entity_stats
ON entity_stats.entity_type = "model_weights"
AND entity_stats.entity_token = mw.token
"#
));
let mut first_predicate_added = false;
match view_as {
ViewAs::Author | ViewAs::Moderator => {
if !first_predicate_added {
query_builder.push(" WHERE ");
first_predicate_added = true;
} else {
query_builder.push(" AND ");
}
// NB(bt): Actually, mods don't want to see deleted models. We'll improve the moderator UI later.
query_builder.push(" mw.user_deleted_at IS NULL AND mw.mod_deleted_at IS NULL ");
}
ViewAs::AnotherUser => {
if !first_predicate_added {
query_builder.push(" WHERE ");
first_predicate_added = true;
} else {
query_builder.push(" AND ");
}
query_builder.push(" mw.user_deleted_at IS NULL AND mw.mod_deleted_at IS NULL ");
query_builder.push(" AND mw.creator_set_visibility = ");
query_builder.push_bind(Visibility::Public.to_str());
}
}
if let Some(weight_types) = maybe_scoped_weight_types {
// NB: `WHERE IN` comma separated syntax will be wrong if list has zero length
// We'll skip the predicate if the list isn't empty.
if !weight_types.is_empty() {
if !first_predicate_added {
query_builder.push(" WHERE ");
first_predicate_added = true;
} else {
query_builder.push(" AND ");
}
query_builder.push(" mw.weights_type IN ( ");
let mut separated = query_builder.separated(", ");
for weight_type in weight_types.iter() {
separated.push_bind(weight_type.to_str());
}
separated.push_unseparated(") ");
}
}
if let Some(weight_categories) = maybe_scoped_weight_categories {
// NB: `WHERE IN` comma separated syntax will be wrong if list has zero length
// We'll skip the predicate if the list isn't empty.
if !weight_categories.is_empty() {
if !first_predicate_added {
query_builder.push(" WHERE ");
first_predicate_added = true;
} else {
query_builder.push(" AND ");
}
query_builder.push(" mw.weights_category IN ( ");
let mut separated = query_builder.separated(", ");
for weights_category in weight_categories.iter() {
separated.push_bind(weights_category.to_str());
}
separated.push_unseparated(") ");
}
}
if let Some(offset) = maybe_offset {
if !first_predicate_added {
query_builder.push(" WHERE ");
first_predicate_added = true;
} else {
query_builder.push(" AND ");
}
let mut sort_ascending = sort_ascending;
if sort_ascending {
if cursor_is_reversed {
// NB: We're searching backwards.
query_builder.push(" m.id < ");
sort_ascending = !sort_ascending;
} else {
query_builder.push(" m.id > ");
}
} else {
if cursor_is_reversed {
// NB: We're searching backwards.
query_builder.push(" m.id > ");
sort_ascending = !sort_ascending;
} else {
query_builder.push(" m.id < ");
}
}
query_builder.push_bind(offset as i64);
}
query_builder
}
async fn map_to_weights(dataset:Vec<RawWeightJoinUser>) -> Vec<FeaturedWeight> {
let weights: Vec<FeaturedWeight> = dataset
.into_iter()
.map(|weight: RawWeightJoinUser| {
FeaturedWeight {
token: weight.token,
title: weight.title,
weights_type: weight.weights_type,
weights_category: weight.weights_category,
maybe_ietf_language_tag: weight.maybe_ietf_language_tag,
maybe_ietf_primary_language_subtag: weight.maybe_ietf_primary_language_subtag,
maybe_creator_user_token: weight.maybe_creator_user_token,
maybe_creator_username: weight.maybe_creator_username,
maybe_creator_display_name: weight.maybe_creator_display_name,
maybe_creator_email_gravatar_hash: weight.maybe_creator_email_gravatar_hash,
creator_ip_address: weight.creator_ip_address,
creator_set_visibility: weight.creator_set_visibility,
maybe_last_update_user_token: weight.maybe_last_update_user_token,
original_download_url: weight.original_download_url,
original_filename: weight.original_filename,
file_size_bytes: weight.file_size_bytes,
file_checksum_sha2: weight.file_checksum_sha2,
public_bucket_hash: weight.public_bucket_hash,
maybe_public_bucket_prefix: weight.maybe_public_bucket_prefix,
maybe_public_bucket_extension: weight.maybe_public_bucket_extension,
maybe_cover_image_public_bucket_hash: weight.maybe_cover_image_public_bucket_hash,
maybe_cover_image_public_bucket_prefix: weight.maybe_cover_image_public_bucket_prefix,
maybe_cover_image_public_bucket_extension: weight.maybe_cover_image_public_bucket_extension,
maybe_ratings_positive_count: weight.maybe_ratings_positive_count,
maybe_ratings_negative_count: weight.maybe_ratings_negative_count,
maybe_bookmark_count: weight.maybe_bookmark_count,
cached_usage_count: weight.cached_usage_count,
version: weight.version,
created_at: weight.created_at,
updated_at: weight.updated_at,
user_deleted_at: weight.user_deleted_at,
mod_deleted_at: weight.mod_deleted_at,
}
}).collect();
weights
}
struct RawWeightJoinUser {
id: i64,
token: ModelWeightToken,
weights_type: WeightsType,
weights_category: WeightsCategory,
title: String,
maybe_ietf_language_tag: Option<String>,
maybe_ietf_primary_language_subtag: Option<String>,
// TODO(bt,2023-12-24): These aren't really appropriate for a list endpoint.
// Hopefully we don't break the frontend by omitting these.
//pub description_markdown: String,
//pub description_rendered_html: String,
maybe_creator_user_token: Option<UserToken>,
maybe_creator_username: Option<String>,
maybe_creator_display_name: Option<String>,
maybe_creator_email_gravatar_hash: Option<String>,
creator_ip_address: String,
creator_set_visibility: Visibility,
maybe_last_update_user_token: Option<UserToken>,
original_download_url: Option<String>,
original_filename: Option<String>,
file_size_bytes: i32,
file_checksum_sha2: String,
public_bucket_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
cached_usage_count: u64,
maybe_cover_image_public_bucket_hash: Option<String>,
maybe_cover_image_public_bucket_prefix: Option<String>,
maybe_cover_image_public_bucket_extension: Option<String>,
maybe_ratings_positive_count: Option<u32>,
maybe_ratings_negative_count: Option<u32>,
maybe_bookmark_count: Option<u32>,
version: i32,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
user_deleted_at: Option<DateTime<Utc>>,
mod_deleted_at: Option<DateTime<Utc>>,
}
impl FromRow<'_, MySqlRow> for RawWeightJoinUser {
fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
Ok(Self {
id: row.try_get("id")?,
token: row.try_get("token")?,
weights_type: row.try_get("weights_type")?,
weights_category: row.try_get("weights_category")?,
title: row.try_get("title")?,
maybe_ietf_language_tag: row.try_get("maybe_ietf_language_tag")?,
maybe_ietf_primary_language_subtag: row.try_get("maybe_ietf_primary_language_subtag")?,
// TODO(bt,2023-12-24): These aren't really appropriate for a list endpoint.
// Hopefully we don't break the frontend by omitting these.
//maybe_description_markdown: row.try_get("maybe_description_markdown")?,
//maybe_description_rendered_html: row.try_get("maybe_description_rendered_html")?,
maybe_creator_user_token: row.try_get("maybe_creator_user_token")?,
maybe_creator_username: row.try_get("maybe_creator_username")?,
maybe_creator_display_name: row.try_get("maybe_creator_display_name")?,
maybe_creator_email_gravatar_hash: row.try_get("maybe_creator_email_gravatar_hash")?,
creator_ip_address: row.try_get("creator_ip_address")?,
creator_set_visibility: Visibility::try_from_mysql_row(row, "creator_set_visibility")?,
maybe_last_update_user_token: row.try_get("maybe_last_update_user_token")?,
original_download_url: row.try_get("original_download_url")?,
original_filename: row.try_get("original_filename")?,
file_size_bytes: row.try_get("file_size_bytes")?,
file_checksum_sha2: row.try_get("file_checksum_sha2")?,
public_bucket_hash: row.try_get("public_bucket_hash")?,
maybe_public_bucket_prefix: row.try_get("maybe_public_bucket_prefix")?,
maybe_public_bucket_extension: row.try_get("maybe_public_bucket_extension")?,
cached_usage_count: row.try_get("cached_usage_count")?,
maybe_cover_image_public_bucket_hash: row.try_get("maybe_cover_image_public_bucket_hash")?,
maybe_cover_image_public_bucket_prefix: row.try_get("maybe_cover_image_public_bucket_prefix")?,
maybe_cover_image_public_bucket_extension: row.try_get("maybe_cover_image_public_bucket_extension")?,
maybe_ratings_positive_count: row.try_get("maybe_ratings_positive_count")?,
maybe_ratings_negative_count: row.try_get("maybe_ratings_negative_count")?,
maybe_bookmark_count: row.try_get("maybe_bookmark_count")?,
version: row.try_get("version")?,
created_at: row.try_get("created_at")?,
updated_at: row.try_get("updated_at")?,
user_deleted_at: row.try_get("user_deleted_at")?,
mod_deleted_at: row.try_get("mod_deleted_at")?,
})
}
}
@@ -1,6 +1,5 @@
pub mod list_all_model_weight_tokens_for_backfill;
pub mod list_all_model_weight_tokens_for_cached_usage_backfill;
pub mod list_featured_weights;
pub mod list_model_weight_tokens_updated_since;
pub mod list_model_weights_for_elastic_search_backfill_using_cursor;
pub mod list_model_weights_for_text_to_speech;
@@ -1,74 +0,0 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use sqlx::MySqlPool;
use errors::AnyhowResult;
#[derive(Serialize)]
pub struct TtsInferenceJobStatusRecord {
pub job_token: String,
pub status: String,
pub attempt_count: i32,
pub maybe_result_token: Option<String>,
pub maybe_public_bucket_wav_audio_path: Option<String>,
pub model_token: String,
pub tts_model_type: String,
pub title: String, // Name of the TTS model
pub raw_inference_text: String, // User text
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn get_tts_inference_job_status(
tts_inference_job_token: &str,
mysql_pool: &MySqlPool
) -> AnyhowResult<Option<TtsInferenceJobStatusRecord>> {
// NB: Lookup failure is Err(RowNotFound).
// NB: Since this is publicly exposed, we don't query sensitive data.
let maybe_status = sqlx::query_as!(
TtsInferenceJobStatusRecord,
r#"
SELECT
jobs.token as job_token,
jobs.status,
jobs.attempt_count,
jobs.on_success_result_token as maybe_result_token,
results.public_bucket_wav_audio_path as maybe_public_bucket_wav_audio_path,
jobs.model_token,
tts.tts_model_type,
tts.title,
jobs.raw_inference_text,
jobs.created_at,
jobs.updated_at
FROM tts_inference_jobs as jobs
JOIN tts_models as tts
ON tts.token = jobs.model_token
LEFT OUTER JOIN tts_results as results
ON jobs.on_success_result_token = results.token
WHERE jobs.token = ?
"#,
tts_inference_job_token
)
.fetch_one(mysql_pool)
.await; // TODO: This will return error if it doesn't exist
match maybe_status {
Ok(record) => Ok(Some(record)),
Err(err) => match err {
sqlx::Error::RowNotFound => Ok(None),
_ => Err(anyhow!("tts job query error: {:?}", err)),
}
}
}
@@ -1,6 +1,5 @@
pub mod _keys;
pub mod get_pending_tts_inference_job_detailed_stats;
pub mod get_tts_inference_job_status;
pub mod insert_tts_inference_job;
pub mod kill_tts_inference_jobs;
pub mod list_available_tts_inference_jobs;
@@ -1,25 +1,15 @@
use std::fmt;
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::web::Json;
use actix_web::http::StatusCode;
use actix_web::web::Path;
use actix_web::{web, HttpMessage, HttpRequest};
use chrono::{DateTime, Utc};
use log::error;
use redis::{Commands, RedisResult};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use mysql_queries::queries::generic_inference::web::get_inference_job_status::get_inference_job_status;
use mysql_queries::queries::tts::tts_inference_jobs::get_tts_inference_job_status::get_tts_inference_job_status;
use redis_common::redis_keys::RedisKeys;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use crate::http_server::web_utils::filter_model_name::filter_model_name;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
/// Terminal status reported for every job so legacy clients stop polling.
const SYNTHETIC_JOB_STATUS: &str = "dead";
const SYNTHETIC_MODEL_TOKEN: &str = "synthetic_model_token";
const SYNTHETIC_MODEL_TITLE: &str = "Legacy TTS is retired";
const SYNTHETIC_MODEL_TYPE: &str = "tacotron2";
/// For the URL PathInfo
#[derive(Deserialize)]
@@ -27,6 +17,12 @@ pub struct GetTtsInferenceStatusPathInfo {
token: String,
}
#[derive(Serialize)]
pub struct GetTtsInferenceStatusSuccessResponse {
pub success: bool,
pub state: TtsInferenceJobStatusForResponse,
}
#[derive(Serialize)]
pub struct TtsInferenceJobStatusForResponse {
pub job_token: String,
@@ -53,17 +49,14 @@ pub struct TtsInferenceJobStatusForResponse {
pub updated_at: DateTime<Utc>,
}
#[derive(Serialize)]
pub struct GetTtsInferenceStatusSuccessResponse {
pub success: bool,
pub state: TtsInferenceJobStatusForResponse,
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
/// RETIRED endpoint that still receives heavy zombie traffic.
///
/// Legacy TTS inference is shut down and the job tables are no longer served.
/// Every job is reported as terminally "dead" (without touching MySQL or
/// Redis) so old clients stop polling.
pub async fn get_tts_inference_job_status_handler(
http_request: HttpRequest,
path: Path<GetTtsInferenceStatusPathInfo>,
server_state: web::Data<Arc<ServerState>>) -> Result<Json<GetTtsInferenceStatusSuccessResponse>, CommonWebError>
{
) -> Result<Json<GetTtsInferenceStatusSuccessResponse>, CommonWebError> {
let job_token = path.into_inner().token;
if job_token.trim() == "None" {
@@ -72,156 +65,23 @@ pub async fn get_tts_inference_job_status_handler(
return Err(CommonWebError::NotFound);
}
// NB(bt,2023-11-27): We're moving TT2 over to `inference-job` (from `tts-inference-job`), which
// uses a wholly different job table. The token prefix determines the type of job:
// Legacy TT2 jobs (tts_inference_jobs) start with "JTINF:", and generic jobs start with "jinf_".
let record_for_response = if job_token.starts_with("jinf_") {
modern_lookup(&job_token, &server_state).await?
} else {
legacy_lookup(&job_token, &server_state).await?
};
let now = Utc::now();
Ok(Json(GetTtsInferenceStatusSuccessResponse {
success: true,
state: record_for_response,
state: TtsInferenceJobStatusForResponse {
job_token,
status: SYNTHETIC_JOB_STATUS.to_string(),
maybe_extra_status_description: None,
attempt_count: 1,
maybe_result_token: None,
maybe_public_bucket_wav_audio_path: None,
model_token: SYNTHETIC_MODEL_TOKEN.to_string(),
tts_model_type: SYNTHETIC_MODEL_TYPE.to_string(),
title: SYNTHETIC_MODEL_TITLE.to_string(),
raw_inference_text: String::new(),
created_at: now,
updated_at: now,
},
}))
}
async fn legacy_lookup(job_token: &str, server_state: &ServerState)
-> Result<TtsInferenceJobStatusForResponse, CommonWebError>
{
// NB: Lookup failure is Err(RowNotFound).
// NB: Since this is publicly exposed, we don't query sensitive data.
let maybe_status =
get_tts_inference_job_status(&job_token, &server_state.mysql_pool).await;
let record = match maybe_status {
Ok(Some(record)) => record,
Ok(None) => return Err(CommonWebError::NotFound),
Err(err) => {
error!("tts job query error: {:?}", err);
return Err(CommonWebError::from_anyhow_error(err));
}
};
let mut redis = server_state.redis_pool
.get()
.map_err(|e| {
error!("redis error: {:?}", e);
CommonWebError::from_error(e)
})?;
// TODO(bt,2023-05-21): Make async.
let extra_status_key = RedisKeys::tts_inference_extra_status_info(&job_token);
let maybe_extra_status_description : Option<String> = match redis.get(&extra_status_key) {
Ok(Some(status)) => {
Some(status)
},
Ok(None) => None,
Err(e) => {
error!("redis error: {:?}", e);
None // Fail open
},
};
Ok(TtsInferenceJobStatusForResponse {
job_token: record.job_token,
status: record.status,
maybe_extra_status_description,
attempt_count: record.attempt_count as u8,
maybe_result_token: record.maybe_result_token,
maybe_public_bucket_wav_audio_path: record.maybe_public_bucket_wav_audio_path,
model_token: record.model_token,
tts_model_type: record.tts_model_type,
title: record.title,
raw_inference_text: record.raw_inference_text,
created_at: record.created_at,
updated_at: record.updated_at,
})
}
async fn modern_lookup(job_token: &str, server_state: &ServerState)
-> Result<TtsInferenceJobStatusForResponse, CommonWebError>
{
// NB: Lookup failure is Err(RowNotFound).
// NB: Since this is publicly exposed, we don't query sensitive data.
let maybe_status =
get_tts_inference_job_status(&job_token, &server_state.mysql_pool).await;
let job_token = InferenceJobToken::new_from_str(job_token);
let maybe_status = get_inference_job_status(&job_token, &server_state.mysql_pool).await;
let record = match maybe_status {
Ok(Some(record)) => record,
Ok(None) => return Err(CommonWebError::NotFound),
Err(err) => {
error!("tts job query error: {:?}", err);
return Err(CommonWebError::from_anyhow_error(err));
}
};
let mut redis = server_state.redis_pool
.get()
.map_err(|e| {
error!("redis error: {:?}", e);
CommonWebError::from_error(e)
})?;
// TODO(bt,2023-05-21): Make async.
let extra_status_key = RedisKeys::generic_inference_extra_status_info(job_token.as_str());
let maybe_extra_status_value : RedisResult<Option<String>> = redis.get(&extra_status_key);
let maybe_extra_status_description = match maybe_extra_status_value {
Err(e) => {
error!("redis error: {:?}", e);
None // Fail open
},
Ok(maybe_value) => match maybe_value.as_deref() {
Some("1") => {
// TODO(bt,2023-10-20): Redis is reporting "1" and it's been surfacing this as a weird
// message to the frontend for months. This needs proper fixing.
None
},
Some(value) => Some(value.to_string()),
None => None,
}
};
// NB: Model type is probably TT2, but let's filter it in case a hidden model type ever sneaks in
let model_type = record.request_details.maybe_model_type.as_deref().unwrap_or_else(|| "tacotron2");
let model_type = filter_model_name(model_type);
Ok(TtsInferenceJobStatusForResponse {
job_token: record.job_token.to_string(),
status: record.status.to_string(),
maybe_extra_status_description,
attempt_count: record.attempt_count as u8,
maybe_result_token: record.maybe_result_details.as_ref().map(|result| result.entity_token.clone()),
maybe_public_bucket_wav_audio_path: record.maybe_result_details.map(|result_details| {
match result_details.entity_type.as_str() {
"media_file" => {
// NB: We're migrating TTS to media_files.
// Zero shot TTS uses media files.
// Legacy TT2 uses old pathing.
MediaFileBucketPath::from_object_hash(
&result_details.public_bucket_location_or_hash,
result_details.maybe_media_file_public_bucket_prefix.as_deref(),
result_details.maybe_media_file_public_bucket_extension.as_deref())
.get_full_object_path_str()
.to_string()
}
_ => {
// NB: TTS results receive the legacy treatment where their table only reports the full bucket path
result_details.public_bucket_location_or_hash
}
}
}),
model_token: record.request_details.maybe_model_token.unwrap_or_else(|| "NO_MODEL_TOKEN".to_string()),
tts_model_type: model_type,
title: record.request_details.maybe_model_title.unwrap_or_else(|| "no model title".to_string()),
raw_inference_text: record.request_details.maybe_raw_inference_text.unwrap_or_else(|| "no inference text".to_string()),
created_at: record.created_at,
updated_at: record.updated_at,
})
}
@@ -1,30 +1,17 @@
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::web::Json;
use actix_web::http::StatusCode;
use actix_web::web::Query;
use actix_web::{web, HttpRequest};
use chrono::{DateTime, Utc};
use log::warn;
use utoipa::{IntoParams, ToSchema};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::common::view_as::ViewAs;
use enums::by_table::model_weights::weights_types::WeightsType;
use mysql_queries::queries::model_weights::list::list_featured_weights::{list_featured_weights, ListFeaturedWeightsArgs};
use primitives::numerics::u64_to_u32_saturating::u64_to_u32_saturating;
use tokens::tokens::model_weights::ModelWeightToken;
use crate::http_server::common_responses::media::weights_cover_image_details::WeightsCoverImageDetails;
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::endpoints::weights::helpers::get_scoped_weights_categories::get_scoped_weights_categories;
use crate::http_server::endpoints::weights::helpers::get_scoped_weights_types::get_scoped_weights_types;
use crate::state::server_state::ServerState;
use crate::util::title_to_url_slug::title_to_url_slug;
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListFeaturedWeightsQueryParams {
@@ -101,9 +88,6 @@ pub struct FeaturedModelWeightForList {
pub updated_at: DateTime<Utc>,
}
/// The key we store featured weights tokens under
const REDIS_KEY : &str = "featured_weights_list";
#[derive(Debug, ToSchema)]
pub enum ListFeaturedWeightsError {
NotAuthorized,
@@ -125,7 +109,10 @@ impl ResponseError for ListFeaturedWeightsError {
}
}
/// List model weights that the staff has selected as "the best" models on the site.
/// RETIRED: featured model weights are shut down.
///
/// Always returns an empty result list (no database access) for the legacy
/// clients that still call this endpoint.
#[utoipa::path(
get,
tag = "Model Weights",
@@ -138,148 +125,10 @@ impl ResponseError for ListFeaturedWeightsError {
),
)]
pub async fn list_featured_weights_handler(
http_request: HttpRequest,
query: Query<ListFeaturedWeightsQueryParams>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<ListFeaturedWeightsSuccessResponse>, impl ResponseError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
ListFeaturedWeightsError::ServerError
})?;
let mut is_mod = false;
let maybe_scoped_weights_types = get_scoped_weights_types(query.filter_weights_types.as_deref());
let maybe_scoped_weights_categories = get_scoped_weights_categories(query.filter_weights_categories.as_deref());
match maybe_user_session {
None => {},
Some(session) => {
is_mod = session.can_ban_users;
},
};
// TODO(bt,2023-12-04): Enforce real maximums and defaults
let limit = query.page_size.unwrap_or(25);
let sort_ascending = query.sort_ascending.unwrap_or(false);
let cursor_is_reversed = query.cursor_is_reversed.unwrap_or(false);
let cursor = if let Some(cursor) = query.cursor.as_deref() {
let cursor = server_state.sort_key_crypto.decrypt_id(cursor)
.map_err(|e| {
warn!("crypto error: {:?}", e);
ListFeaturedWeightsError::ServerError
})?;
Some(cursor as usize)
} else {
None
};
let view_as = if is_mod {
ViewAs::Moderator
} else {
ViewAs::AnotherUser
};
let query_results = list_featured_weights(ListFeaturedWeightsArgs {
limit,
maybe_offset: cursor,
sort_ascending,
cursor_is_reversed,
view_as,
maybe_scoped_weight_types: maybe_scoped_weights_types.as_ref(),
maybe_scoped_weight_categories: maybe_scoped_weights_categories.as_ref(),
mysql_pool: &server_state.mysql_pool,
}).await;
let results_page = match query_results {
Ok(results) => results,
Err(e) => {
warn!("Query error: {:?}", e);
return Err(ListFeaturedWeightsError::ServerError);
}
};
let cursor_next = if let Some(id) = results_page.last_id {
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)
.map_err(|e| {
warn!("crypto error: {:?}", e);
ListFeaturedWeightsError::ServerError
})?;
Some(cursor)
} else {
None
};
let cursor_previous = if let Some(id) = results_page.first_id {
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)
.map_err(|e| {
warn!("crypto error: {:?}", e);
ListFeaturedWeightsError::ServerError
})?;
Some(cursor)
} else {
None
};
let media_domain = get_media_domain(&http_request);
let response = ListFeaturedWeightsSuccessResponse {
_query: Query<ListFeaturedWeightsQueryParams>,
) -> Result<Json<ListFeaturedWeightsSuccessResponse>, ListFeaturedWeightsError> {
Ok(Json(ListFeaturedWeightsSuccessResponse {
success: true,
results: results_page.records.into_iter()
.map(|w| {
let cover_image_details = WeightsCoverImageDetails::from_optional_db_fields(
media_domain,
server_state.server_environment,
&w.token,
w.maybe_cover_image_public_bucket_hash.as_deref(),
w.maybe_cover_image_public_bucket_prefix.as_deref(),
w.maybe_cover_image_public_bucket_extension.as_deref(),
);
let maybe_cover_image = w.maybe_cover_image_public_bucket_hash
.as_deref()
.map(|hash| {
MediaFileBucketPath::from_object_hash(
hash,
w.maybe_cover_image_public_bucket_prefix.as_deref(),
w.maybe_cover_image_public_bucket_extension.as_deref())
.get_full_object_path_str()
.to_string()
});
FeaturedModelWeightForList {
weight_token: w.token,
maybe_url_slug: title_to_url_slug(&w.title),
title: w.title,
maybe_ietf_language_tag: w.maybe_ietf_language_tag,
maybe_ietf_primary_language_subtag: w.maybe_ietf_primary_language_subtag,
weight_type: w.weights_type,
weight_category: w.weights_category,
cover_image: cover_image_details,
maybe_cover_image_public_bucket_path: maybe_cover_image,
creator: UserDetailsLight::from_optional_db_fields(
w.maybe_creator_user_token.as_ref(),
w.maybe_creator_username.as_deref(),
w.maybe_creator_display_name.as_deref(),
w.maybe_creator_email_gravatar_hash.as_deref(),
),
stats: SimpleEntityStats {
positive_rating_count: w.maybe_ratings_positive_count.unwrap_or(0),
bookmark_count: w.maybe_bookmark_count.unwrap_or(0),
},
usage_count: u64_to_u32_saturating(w.cached_usage_count),
created_at: w.created_at,
updated_at: w.updated_at,
}
}).collect::<Vec<_>>(),
};
Ok(Json(response))
results: Vec::new(),
}))
}
@@ -3,7 +3,6 @@
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use std::collections::HashSet;
use std::fmt;
use std::sync::Arc;
@@ -12,23 +11,18 @@ use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::{web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use log::error;
use utoipa::ToSchema;
use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::by_table::model_weights::weights_types::WeightsType;
use enums::common::visibility::Visibility;
use tokens::tokens::model_weights::ModelWeightToken;
use crate::http_server::common_responses::media::weights_cover_image_details::WeightsCoverImageDetails;
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use crate::util::title_to_url_slug::title_to_url_slug;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use elasticsearch_schema::searches::search_model_weights::search_model_weights::{search_model_weights, ModelWeightsSortDirection, ModelWeightsSortField, SearchArgs};
use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::by_table::model_weights::weights_types::WeightsType;
use enums::common::visibility::Visibility;
use primitives::numerics::i32_to_u32_zero_clamped::i32_to_u32_zero_clamped;
use tokens::tokens::model_weights::ModelWeightToken;
#[derive(Deserialize, ToSchema)]
pub struct SearchModelWeightsRequest {
@@ -63,6 +57,12 @@ pub enum SearchModelWeightsSortDirection {
Descending,
}
#[derive(Serialize, ToSchema)]
pub struct SearchModelWeightsSuccessResponse {
pub success: bool,
pub weights: Vec<ModelWeightSearchResult>,
}
#[derive(Serialize, Clone, ToSchema)]
pub struct ModelWeightSearchResult {
pub weight_token: ModelWeightToken,
@@ -108,12 +108,6 @@ pub struct ModelWeightSearchResult {
pub updated_at: DateTime<Utc>,
}
#[derive(Serialize, ToSchema)]
pub struct SearchModelWeightsSuccessResponse {
pub success: bool,
pub weights: Vec<ModelWeightSearchResult>,
}
#[derive(Debug, ToSchema)]
pub enum SearchModelWeightsError {
ServerError,
@@ -142,128 +136,18 @@ impl fmt::Display for SearchModelWeightsError {
}
}
/// RETIRED: model weight search is shut down.
///
/// Always returns an empty result list (no Elasticsearch access) for the
/// legacy clients that still call the search endpoints.
pub async fn search_model_weights_impl(
http_request: HttpRequest,
request: SearchModelWeightsRequest,
server_state: web::Data<Arc<ServerState>>
_http_request: HttpRequest,
_request: SearchModelWeightsRequest,
_server_state: web::Data<Arc<ServerState>>
) -> Result<Json<SearchModelWeightsSuccessResponse>, SearchModelWeightsError>
{
let maybe_weights_categories = request.weight_category
.map(|weight_category| {
let mut set = HashSet::new();
set.insert(weight_category);
set
});
let maybe_weights_types = request.weight_type
.map(|weight_type| {
let mut set = HashSet::new();
set.insert(weight_type);
set
});
let sort_field = match request.sort_field {
Some(SearchModelWeightsSortField::MatchScore) => Some(ModelWeightsSortField::MatchScore),
Some(SearchModelWeightsSortField::CreatedAt) => Some(ModelWeightsSortField::CreatedAt),
Some(SearchModelWeightsSortField::UsageCount) => Some(ModelWeightsSortField::UsageCount),
Some(SearchModelWeightsSortField::BookmarkCount) => Some(ModelWeightsSortField::BookmarkCount),
Some(SearchModelWeightsSortField::PositiveRatingCount) => Some(ModelWeightsSortField::PositiveRatingCount),
None => None,
};
let sort_direction = match request.sort_direction {
Some(SearchModelWeightsSortDirection::Ascending) => Some(ModelWeightsSortDirection::Ascending),
Some(SearchModelWeightsSortDirection::Descending) => Some(ModelWeightsSortDirection::Descending),
None => None,
};
let results = search_model_weights(SearchArgs {
search_term: &request.search_term,
maybe_creator_user_token: None,
maybe_ietf_primary_language_subtag: request.ietf_language_subtag.as_deref(),
maybe_weights_categories,
maybe_weights_types,
sort_field,
sort_direction,
minimum_score: request.minimum_score,
client: &server_state.elasticsearch,
})
.await
.map_err(|err| {
error!("Searching error: {:?}", err);
SearchModelWeightsError::ServerError
})?;
let media_domain = get_media_domain(&http_request);
let results = results.into_iter()
.map(|result| {
let cover_image_details = WeightsCoverImageDetails::from_optional_db_fields(
media_domain,
server_state.server_environment,
&result.token,
result.maybe_cover_image_public_bucket_hash.as_deref(),
result.maybe_cover_image_public_bucket_prefix.as_deref(),
result.maybe_cover_image_public_bucket_extension.as_deref(),
);
let maybe_cover_image = result.maybe_cover_image_public_bucket_hash
.as_deref()
.map(|hash| {
MediaFileBucketPath::from_object_hash(
&hash,
result.maybe_cover_image_public_bucket_prefix.as_deref(),
result.maybe_cover_image_public_bucket_extension.as_deref())
.get_full_object_path_str()
.to_string()
});
ModelWeightSearchResult {
weight_token: result.token,
weight_type: result.weights_type,
weight_category: result.weights_category,
maybe_url_slug: title_to_url_slug(&result.title),
title: result.title,
creator: UserDetailsLight::from_db_fields(
&result.creator_user_token,
&result.creator_username,
&result.creator_display_name,
&result.creator_gravatar_hash,
),
cover_image: cover_image_details,
maybe_cover_image_public_bucket_path: maybe_cover_image,
is_featured: result.is_featured.unwrap_or(false),
stats: SimpleEntityStats {
positive_rating_count: result.ratings_positive_count,
bookmark_count: result.bookmark_count,
},
usage_count: i32_to_u32_zero_clamped(result.cached_usage_count.unwrap_or(0)),
maybe_ietf_language_tag: result.maybe_ietf_language_tag,
maybe_ietf_primary_language_subtag: result.maybe_ietf_primary_language_subtag,
creator_set_visibility: result.creator_set_visibility,
created_at: result.created_at,
updated_at: result.updated_at,
}
})
.collect::<Vec<_>>();
// TODO(bt,2023-10-27): For some reason Elasticsearch returns duplicates. Maybe we populated the
// DB twice? Need to filter them out, or React chokes and gets stuck on duplicates. (Effectively
// freezing them into the UI, despite component updates)
let mut added_tokens = HashSet::new();
let mut new_results = Vec::with_capacity(results.len());
for result in results.into_iter() {
if added_tokens.contains(&result.weight_token) {
continue;
}
added_tokens.insert(result.weight_token.clone());
new_results.push(result);
}
Ok(Json(SearchModelWeightsSuccessResponse {
success: true,
weights: new_results,
weights: Vec::new(),
}))
}