Ok inital commit for server side need to run inference on the specific model once downloaded

to verify that the model is compatible with our infrastructure.
This commit is contained in:
Michael Chung committed 2023-12-11 15:11:28 -05:00
1 parent 602de6d736
commit e190721ece
3 files changed
+158 -332

No files matched your search

@@ -3,11 +3,16 @@
use crate::is_bad_download_url::is_bad_download_url;
/// Reports if the URL is inappropriate for downloading as a TTS model, vocoder, etc.
/// DEPRECATE:
pub fn is_bad_tts_model_download_url(url: &str) -> anyhow::Result<bool> {
// TODO: Just use this.
is_bad_download_url(url)
}
pub fn is_bad_model_weights_download_url(url: &str) -> anyhow::Result<bool> {
is_bad_download_url(url)
}
#[cfg(test)]
mod tests {
use crate::bad_urls::is_bad_tts_model_download_url;
@@ -1,100 +0,0 @@
use actix_multipart::Multipart;
use actix_web::web::BytesMut;
use futures::TryStreamExt;
use log::warn;
use errors::AnyhowResult;
use tokens::tokens::weights::ModelWeightToken;
use crate::http_server::web_utils::read_multipart_field_bytes::{checked_read_multipart_bytes, read_multipart_field_as_text};
pub struct WeightUploadRequest {
pub uuid_idempotency_token: Option<String>,
pub model_weights_token: Option<String>,
pub file_name: Option<String>,
pub file_bytes: Option<BytesMut>,
pub media_source: MediaSource,
}
/// Where the frontend tells us the file came from.
#[derive(Eq, PartialEq, Clone, Copy)]
pub enum MediaSource {
Unknown,
UserFile,
/// Eg. the web audio API in Javascript.
UserDeviceApi,
}
/// Pull common parts out of multipart media HTTP requests, typically for handling file uploads.
pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> AnyhowResult<WeightUploadRequest> {
let mut uuid_idempotency_token = None;
let mut model_weight_token = None;
let mut file_bytes = None;
let mut file_name = None;
let mut media_source = None;
while let Ok(Some(mut field)) = multipart_payload.try_next().await {
let mut field_name = None;
let mut field_filename = None;
if let content_disposition = field.content_disposition() {
field_name = content_disposition.get_name()
.map(|s| s.to_string());
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
.map(|s| s.to_string());
}
match field_name.as_deref() {
Some("uuid_idempotency_token") => {
uuid_idempotency_token = read_multipart_field_as_text(&mut field).await
.map_err(|e| {
warn!("Error reading idempotency token: {:}", e);
e
})?;
},
Some("dataset_token") => {
model_weight_token = read_multipart_field_as_text(&mut field).await
.map_err(|e| {
warn!("Error reading dataset token: {:}", e);
e
})?;
},
Some("file") => {
file_name = field_filename.clone();
file_bytes = checked_read_multipart_bytes(&mut field).await
.map_err(|e| {
warn!("Error reading audio upload: {:}", e);
e
})?;
},
Some("source") => {
media_source = read_multipart_field_as_text(&mut field).await
.map_err(|e| {
warn!("Error reading source: {:}", e);
e
})?;
},
_ => continue,
}
}
let media_source = match media_source.as_deref() {
Some("device") => MediaSource::UserDeviceApi,
Some("file") => MediaSource::UserFile,
_ => {
if file_name.as_deref() == Some("blob") {
MediaSource::UserDeviceApi
} else {
MediaSource::Unknown
}
},
};
Ok(UploadSampleRequest {
uuid_idempotency_token,
dataset_token,
file_name,
file_bytes,
media_source,
})
}
@@ -2,10 +2,10 @@ use std::collections::HashSet;
use std::sync::Arc;
use actix_multipart::Multipart;
use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::{ HttpRequest, HttpResponse, web };
use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::by_table::model_weights::weights_types::WeightsType;
use mysql_queries::queries::model_weights::create_weight::{self, create_weight};
use mysql_queries::queries::model_weights::create_weight::{ self, create_weight };
use once_cell::sync::Lazy;
use tokens::tokens::model_weights::ModelWeightToken;
@@ -13,259 +13,180 @@ use tokens::tokens::model_weights::ModelWeightToken;
use crate::http_server::endpoints::media_uploads::common::upload_error::UploadError;
use crate::server_state::ServerState;
use log::{error, info, warn};
use log::{ error, info, warn };
use buckets::public::weight_uploads::original_file::WeightUploadOriginalFilePath;
use enums::by_table::media_uploads::media_upload_source::MediaUploadSource;
use enums::by_table::media_uploads::media_upload_type::MediaUploadType;
use enums::common::visibility::Visibility;
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use http_server_common::request::get_request_ip::get_request_ip;
use media::decode_basic_audio_info::decode_basic_audio_bytes_info;
use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes;
use mysql_queries::queries::media_uploads::get_media_upload_by_uuid::get_media_upload_by_uuid_with_connection;
use mysql_queries::queries::media_uploads::insert_media_upload::{Args, insert_media_upload};
use mysql_queries::queries::media_uploads::insert_media_upload::{ Args, insert_media_upload };
use crate::http_server::endpoints::media_uploads::common::drain_multipart_request::{drain_multipart_request, MediaSource};
use crate::http_server::endpoints::media_uploads::common::drain_multipart_request::{
drain_multipart_request,
MediaSource,
};
use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use utoipa::ToSchema;
use config::bad_urls::is_bad_model_weights_download_url;
use enums::by_table::model_weights::{
weights_category::WeightsCategory,
weights_types::WeightsType,
};
use enums::common::visibility::Visibility;
use utoipa::ToSchema;
#[derive(Deserialize, ToSchema)]
pub struct UploadModelWeightsRequest {
idempotency_token: String,
title: String,
download_url: String,
model_weights_type: WeightsType,
model_weights_category: WeightsCategory,
creator_set_visibility: Option<Visibility>,
}
/// Tell the frontend how to deal with the download queue.
#[derive(Serialize, ToSchema)]
pub enum DownloadJobType {
/// Modern shared download type (everything will use this in the
/// future, and this endpoint will die.)
#[serde(rename = "generic")]
Generic,
}
#[derive(Serialize, ToSchema)]
pub struct UploadModelWeightsSuccessResponse {
pub success: bool,
/// This is how frontend clients can request the job execution status.
pub job_token: String,
/// This is a transitional field to tell the frontend how to process the job checking.
pub job_type: DownloadJobType,
}
#[derive(Debug)]
pub enum UploadModelWeightsError {
BadInput(String),
MustBeLoggedIn,
ServerError,
RateLimited,
}
impl ResponseError for UploadModelWeightsError {
fn status_code(&self) -> StatusCode {
match *self {
UploadModelWeightsError::BadInput(_) => StatusCode::BAD_REQUEST,
UploadModelWeightsError::MustBeLoggedIn => StatusCode::UNAUTHORIZED,
UploadModelWeightsError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
UploadModelWeightsError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
UploadModelWeightsError::BadInput(reason) => reason.to_string(),
UploadModelWeightsError::MustBeLoggedIn => "user must be logged in".to_string(),
UploadModelWeightsError::ServerError => "server error".to_string(),
UploadModelWeightsError::RateLimited => "rate limited".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
pub async fn upload_weights_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
mut multipart_payload: Multipart,
) -> Result<HttpResponse, UploadError> {
let response = handle_weight_upload(
&http_request,
&server_state,
multipart_payload,
&ALLOWED_MIME_TYPES).await?;
// send a body with the required payload along side the multipart upload
let weight_upload_token = response.to_model_token();
let response: UploadWeightsSuccessResponse = UploadWeightsSuccessResponse {
success: true,
upload_token: weight_upload_token,
};
let body = serde_json::to_string(&response)
.map_err(|e| UploadError::ServerError)?;
return Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body));
}
pub enum SuccessCase {
WeightAlreadyUploaded {
existing_upload_token: ModelWeightToken,
},
WeightSuccessfullyUploaded {
upload_token: ModelWeightToken,
mut multipart_payload: Multipart
) -> Result<HttpResponse, UploadError> {
// Rate limiter check
if
let Err(_err) = server_state.redis_rate_limiters.model_upload.rate_limit_request(
&http_request
)
{
return Err(UploadModelWeightsError::RateLimited);
}
}
impl SuccessCase {
pub fn to_model_token(self) -> ModelWeightToken {
match self {
SuccessCase::WeightAlreadyUploaded { existing_upload_token } => existing_upload_token,
SuccessCase::WeightSuccessfullyUploaded { upload_token } => upload_token,
}
}
}
pub async fn handle_weight_upload(
http_request: &HttpRequest,
server_state: &web::Data<Arc<ServerState>>,
mut multipart_payload: Multipart,
allowed_mimetypes: &HashSet<&'static str>,
) -> Result<SuccessCase, UploadError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
error!("MySql pool error: {:?}", err);
UploadError::ServerError
})?;
// ==================== READ SESSION ==================== //
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(http_request, &mut mysql_connection)
.await
let maybe_user_session = server_state.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool).await
.map_err(|e| {
error!("Session checker error: {:?}", e);
UploadError::ServerError
warn!("Session checker error: {:?}", e);
UploadModelWeightsError::ServerError
})?;
// ==================== RATE LIMIT ==================== //
let rate_limiter = match maybe_user_session {
None => &server_state.redis_rate_limiters.logged_out,
Some(ref user) => {
if user.is_banned {
return Err(UploadError::NotAuthorized);
let user_session = match maybe_user_session {
Some(session) => session,
None => {
warn!("not logged in");
return Err(UploadModelWeightsError::MustBeLoggedIn);
}
&server_state.redis_rate_limiters.logged_in
},
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request) {
return Err(UploadError::RateLimited);
if let Err(reason) = validate_idempotency_token(&request.idempotency_token) {
return Err(UploadModelWeightsError::BadInput(reason));
}
if let Err(_err) = server_state.redis_rate_limiters.model_upload.rate_limit_request(&http_request) {
return Err(UploadError::RateLimited);
if let Err(reason) = validate_model_title(&request.title) {
return Err(UploadModelWeightsError::BadInput(reason));
}
// ==================== READ MULTIPART REQUEST ==================== //
let upload_weights_request = drain_multipart_request(multipart_payload)
.await
.map_err(|e| {
// TODO: Error handling could be nicer.
UploadError::BadInput("bad request".to_string())
})?;
let uuid_idempotency_token = upload_weights_request.uuid_idempotency_token
.ok_or(UploadError::BadInput("no uuid".to_string()))?;
let maybe_existing_upload =
get_media_upload_by_uuid_with_connection(&uuid_idempotency_token, &mut mysql_connection)
.await;
match maybe_existing_upload {
Err(err) => {
error!("Error checking for previous upload: {:?}", err);
return Err(UploadError::ServerError);
}
Ok(Some(upload)) => {
// File already uploaded and frontend didn't protect us.
return Ok(SuccessCase::WeightAlreadyUploaded {
existing_upload_token: upload.token,
});
}
Ok(None) => {
// Proceed.
}
match is_bad_model_weights_download_url(&download_url) {
Ok(false) => {} // Ok case
Ok(true) => {
return Err(UploadModelWeightsError::BadInput("Bad model download URL".to_string()));
}
Err(err) => {
warn!("Error parsing url: {:?}", err);
return Err(UploadModelWeightsError::BadInput("Bad model download URL".to_string()));
}
}
if let Err(reason) = validate_idempotency_token_format(&uuid_idempotency_token) {
return Err(UploadError::BadInput(reason));
}
let creator_set_visibility = maybe_user_session
.as_ref()
.map(|user_session| user_session.preferred_tts_result_visibility) // TODO: We need a new type of visibility control.
.unwrap_or(Visibility::default());
let ip_address = get_request_ip(&http_request);
let maybe_user_token = maybe_user_session
.map(|session| session.get_strongly_typed_user_token());
let maybe_file_size_bytes = upload_weights_request.file_bytes
.as_ref()
.map(|bytes| bytes.len());
info!("Upload maybe filesize: {:?}", maybe_file_size_bytes);
let maybe_mimetype = upload_weights_request.file_bytes
.as_ref()
.map(|bytes| get_mimetype_for_bytes(bytes.as_ref()))
.flatten();
let bytes = match upload_weights_request.file_bytes {
None => return Err(UploadError::BadInput("missing file contents".to_string())),
Some(bytes) => bytes,
let supported_model_weights_type = request.model_weights_type;
let job_token;
let download_job_type;
let (download_job_token, _record_id) = insert_generic_download_job(
InsertGenericDownloadJobArgs {
uuid_idempotency_token: &uuid,
download_type: GenericDownloadType::Vits,
download_url: &download_url,
title: &title,
creator_user_token: &user_session.user_token,
creator_ip_address: &ip_address,
creator_set_visibility: visibility, // TODO: Creator set preference
mysql_pool: &server_state.mysql_pool,
}
).await.map_err(|err| {
warn!("New generic download creation DB error (for generic model): {:?}", err);
UploadModelWeightsError::ServerError
})?;
job_token = download_job_token.to_string();
download_job_type = DownloadJobType::Generic;
let response = UploadModelWeightsSuccessResponse {
success: true,
job_token: job_token.to_string(),
job_type: download_job_type,
};
let hash = sha256_hash_bytes(&bytes)
.map_err(|io_error| {
error!("Problem hashing bytes: {:?}", io_error);
UploadError::ServerError
})?;
let file_size_bytes = bytes.len();
let mut maybe_duration_millis = None;
let mut maybe_codec_name = None;
let mut media_upload_type = None;
if let Some(mimetype) = maybe_mimetype.as_deref() {
if !allowed_mimetypes.contains(mimetype) {
// NB: Don't let our error message inject malicious strings
let filtered_mimetype = mimetype
.chars()
.filter(|c| c.is_ascii())
.filter(|c| c.is_alphanumeric() || *c == '/')
.collect::<String>();
return Err(UploadError::BadInput(format!("unpermitted mime type: {}", &filtered_mimetype)));
}
media_upload_type = match mimetype {
"binary/octet-stream" /* .bin */ => Some(MediaUploadType::Binary),
_ => None,
};
let body = serde_json::to_string(&response).map_err(|_e| UploadModelWeightsError::ServerError)?;
Ok(HttpResponse::Ok().content_type("application/json").body(body))
}
fn validate_idempotency_token(token: &str) -> Result<(), String> {
if token.len() != 36 {
return Err("idempotency token should be 36 characters".to_string());
}
// This is fine for now we just don't expose the path!
let public_upload_path = WeightUploadOriginalFilePath::generate_new();
info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str());
// need to upload based on visibility
server_state.public_bucket_client.upload_file_with_content_type(
public_upload_path.get_full_object_path_str(),
bytes.as_ref(),
mime_type)
.await
.map_err(|e| {
warn!("Upload media bytes to bucket error: {:?}", e);
UploadError::ServerError
})?;
let token = create_weight(CreateModelWeightsArgs {
token: &ModelWeightToken::generate(),
weights_type: WeightsType::LoRA, // to do parse multi part for this
weights_category: WeightsCategory::ImageGeneration,
title: upload_weights_request.file_name.as_deref(),
maybe_thumbnail_token: &ThumbnailToken::generate(),
description_markdown: "",
description_rendered_html: "",
creator_user_token: maybe_user_token.as_str(),
creator_ip_address: &ip_address,
creator_set_visibility,
maybe_last_update_user_token: "",
original_download_url: "",
original_filename: upload_weights_request.file_name.as_deref(),
file_size_bytes: file_size_bytes as i32,
file_checksum_sha2: hash,
private_bucket_hash: private_bucket_hash,
maybe_private_bucket_prefix: None,
maybe_private_bucket_extension: None,
cached_user_ratings_total_count: 0,
cached_user_ratings_positive_count: 0,
cached_user_ratings_negative_count: 0,
maybe_cached_user_ratings_ratio: None,
version: 0,
mysql_pool: &server_state.mysql_pool,
}).await
.map_err(|e| {
error!("Error creating weights upload: {:?}", e);
UploadError::ServerError
})?;
info!("new weights upload id: {} token: {:?}", record_id, &token);
Ok(SuccessCase::WeightSuccessfullyUploaded {
upload_token: token,
})
}
Ok(())
}