From b649bb17c5a88d718909a548c70e5a6dd5955b4a Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 14 Dec 2022 00:13:12 -0500 Subject: [PATCH 01/54] Web Voice Conversion --- .../down.sql | 5 + .../up.sql | 150 ++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 _migrations/2022-12-14-050849_generic_inference_jobs/down.sql create mode 100644 _migrations/2022-12-14-050849_generic_inference_jobs/up.sql diff --git a/_migrations/2022-12-14-050849_generic_inference_jobs/down.sql b/_migrations/2022-12-14-050849_generic_inference_jobs/down.sql new file mode 100644 index 0000000000..78a9716f01 --- /dev/null +++ b/_migrations/2022-12-14-050849_generic_inference_jobs/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE generic_inference_jobs; \ No newline at end of file diff --git a/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql b/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql new file mode 100644 index 0000000000..b215894dfe --- /dev/null +++ b/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql @@ -0,0 +1,150 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE generic_inference_jobs ( + + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + -- This is so the in-progress results can be looked up by the UI. + token VARCHAR(32) NOT NULL, + + -- Idempotency token from client + -- This is so the frontend client doesn't submit duplicate jobs. + uuid_idempotency_token VARCHAR(36) NOT NULL, + + -- ========== INFERENCE DETAILS ========== + + -- Type of inference + inference_type VARCHAR(32) NOT NULL, + + -- Polymorphic arguments payload that depends on the type of inference job. + -- TEXT = 65,535 bytes (64 KiB), ~= 4 bytes per UTF-8 character, ~ 16383 characters. + maybe_inference_args TEXT DEFAULT NULL, + + -- For text-based workloads, the raw, unprocessed user input. + -- TEXT = 65,535 bytes (64 KiB), ~= 4 bytes per UTF-8 character, ~ 16383 characters. + maybe_raw_inference_text TEXT DEFAULT NULL, + + -- Potential foreign key to the primary model being used, if any. + -- This will normally live in `maybe_inference_args`, but in this case, it's useful for + -- running easy database analytical queries. + maybe_model_token VARCHAR(32) DEFAULT NULL, + + -- ========== SUCCESS CASE ========== + + -- The type of the object will vary based on the type of the upload, + -- and we may include heuristics that auto-detect types in the future + on_success_result_entity_type VARCHAR(32) DEFAULT NULL, + + -- If the job completes successfully, this is the result token. + -- This is only populated on a successful result. + on_success_result_entity_token VARCHAR(32) DEFAULT NULL, + + -- ========== CREATOR DETAILS AND PREFERENCES ========== + + -- Foreign key to user + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + -- The creator can set a desired visibility for their data. + -- This does not always apply to every upload type. + -- Additionally, some upload types may require moderator approval prior + -- to being publicly listed, and this field has no bearing on that. + -- NB: DO NOT CHANGE THE ORDER OF THE ENUM VALUES DURING SCHEMA MIGRATIONS. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + -- ========== PREMIUM FEATURES METADATA ========== + + -- The maximum duration for generated audio in seconds. + -- Zero is implied to be the default value, which is typically 12 seconds. + -- A negative value implies "unlimited". + max_duration_seconds INTEGER NOT NULL DEFAULT 0, + + -- ========== WORKLOAD SOURCE AND PRIORITY ========== + + -- Priority *increases*, so a level of 2 will be higher than 1. + -- These are the level semantics currently: + -- - All jobs from anonymous FakeYou users have level 0. + -- - All jobs from logged in FakeYou users have level 1. + -- - All jobs from Twitch TTS (unpaid) have level 10 (ten). + -- - (There will be future levels for paid Twitch and social FakeYou rewards.) + priority_level TINYINT UNSIGNED NOT NULL DEFAULT 0, + + is_from_premium_user BOOLEAN NOT NULL DEFAULT FALSE, + is_from_api_user BOOLEAN NOT NULL DEFAULT FALSE, + is_for_twitch BOOLEAN NOT NULL DEFAULT FALSE, + + -- If true, the request gets routed to a special "debug" worker. + is_debug_request BOOLEAN NOT NULL DEFAULT FALSE, + + -- ========== JOB SYSTEM DETAILS ========== + + -- Jobs begin as "pending", then transition to other states. + -- + -- * Pending = job is ready to go + -- * Started = job is running + -- * Complete_Success = job is done (success) + -- * Complete_Failure = job is done (failure) + -- * Attempt_Failed = job failed but may retry. + -- * Dead = job failed permanently. + -- + -- Pending -> Started -> Complete_Success + -- |-> Complete_Failure + -- \-> Attempt_Failed -> Started -> { Complete, Failed, Dead } + status ENUM( + 'pending', + 'started', + 'complete_success', + 'complete_failure', + 'attempt_failed', + 'dead') NOT NULL DEFAULT 'pending', + + -- We can track this against a "max_attempt_count" + attempt_count INT(3) NOT NULL DEFAULT 0, + + -- If there is a failure, tell the user why. + failure_reason VARCHAR(512) DEFAULT NULL, + + -- Optional internal-only debugging information in the case of failure. + internal_debugging_failure_reason VARCHAR(512) DEFAULT NULL, + + -- The last worker (hostname or pod name) to touch the job, either in the case of success or failure. + last_assigned_worker VARCHAR(255) DEFAULT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Failed jobs will set a next attempt time. + -- Subsequent tries can increase the timeout. + -- Failures because of permissions require human intervention => [retry_at=null]. + -- Failures because of invalid files are dead => [status=dead]. + retry_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + UNIQUE KEY (uuid_idempotency_token), + KEY index_inference_type (inference_type), + KEY fk_maybe_model_token (maybe_model_token), + KEY fk_on_success_result_entity_token (on_success_result_entity_token), + KEY fk_maybe_creator_user_token (maybe_creator_user_token), + KEY index_creator_ip_address (creator_ip_address), + KEY index_priority_level (priority_level), + KEY index_is_debug_request (is_debug_request), + KEY index_status (status) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; From 1de10d1b9fa740888afc67208e15e46cf89242e6 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 14 Dec 2022 18:14:21 -0500 Subject: [PATCH 02/54] Move all service/web/job projects into grouped subdirectories by type --- Cargo.toml | 20 +++++------ .../service/{ => job}/download_job/Cargo.toml | 20 +++++------ .../{ => job}/download_job/src/job_state.rs | 0 .../download_job/src/job_steps/job_results.rs | 0 .../download_job/src/job_steps/main_loop.rs | 0 .../download_job/src/job_steps/mod.rs | 0 .../src/job_steps/process_single_job.rs | 0 .../hifigan/hifigan_model_check_command.rs | 0 .../download_job/src/job_types/hifigan/mod.rs | 0 .../hifigan/process_hifigan_vocoder.rs | 0 .../download_job/src/job_types/mod.rs | 0 .../src/job_types/tacotron/mod.rs | 0 .../tacotron/process_tacotron_model.rs | 0 .../tacotron/tacotron_model_check_command.rs | 0 .../download_job/src/job_types/talknet/mod.rs | 0 .../talknet/talknet_model_check_command.rs | 0 .../{ => job}/download_job/src/main.rs | 0 .../{ => job}/inference_job/Cargo.toml | 16 ++++----- .../src/caching/cache_miss_strategizer.rs | 0 .../caching/cache_miss_strategizer_multi.rs | 0 .../inference_job/src/caching/mod.rs | 0 .../src/caching/virtual_lfu_cache.rs | 0 .../inference_job/src/http_clients/mod.rs | 0 .../tts_inference_sidecar_client.rs | 0 .../tts_sidecar_health_check_client.rs | 0 .../job_steps/download_file_from_bucket.rs | 0 .../src/job_steps/health_check_trap.rs | 0 .../inference_job/src/job_steps/job_args.rs | 0 .../inference_job/src/job_steps/job_stats.rs | 0 .../inference_job/src/job_steps/mod.rs | 0 .../src/job_steps/process_single_job.rs | 0 .../src/job_steps/process_single_job_error.rs | 0 .../src/job_steps/seconds_to_decoder_steps.rs | 0 .../{ => job}/inference_job/src/main.rs | 0 .../inference_job/src/script_execution/mod.rs | 0 .../tacotron_inference_command.rs | 0 .../{ => job}/inference_job/src/util/mod.rs | 0 .../src/util/scoped_temp_dir_creator.rs | 0 .../{ => job}/tts_download_job/Cargo.toml | 16 ++++----- .../{ => job}/tts_download_job/src/main.rs | 0 .../src/script_execution/mod.rs | 0 .../tacotron_model_check_command.rs | 0 .../talknet_model_check_command.rs | 0 .../{ => job}/tts_inference_job/Cargo.toml | 16 ++++----- .../src/caching/cache_miss_strategizer.rs | 0 .../caching/cache_miss_strategizer_multi.rs | 0 .../tts_inference_job/src/caching/mod.rs | 0 .../src/caching/virtual_lfu_cache.rs | 0 .../tts_inference_job/src/http_clients/mod.rs | 0 .../tts_inference_sidecar_client.rs | 0 .../tts_sidecar_health_check_client.rs | 0 .../job_steps/download_file_from_bucket.rs | 0 .../src/job_steps/health_check_trap.rs | 0 .../src/job_steps/job_args.rs | 0 .../src/job_steps/job_stats.rs | 0 .../tts_inference_job/src/job_steps/mod.rs | 0 .../src/job_steps/process_single_job.rs | 0 .../src/job_steps/process_single_job_error.rs | 0 .../src/job_steps/seconds_to_decoder_steps.rs | 0 .../{ => job}/tts_inference_job/src/main.rs | 0 .../src/script_execution/mod.rs | 0 .../tacotron_inference_command.rs | 0 .../tts_inference_job/src/util/mod.rs | 0 .../src/util/scoped_temp_dir_creator.rs | 0 .../{ => job}/w2l_download_job/Cargo.toml | 16 ++++----- .../{ => job}/w2l_download_job/src/main.rs | 0 .../ffmpeg_generate_preview_image_command.rs | 0 .../ffmpeg_generate_preview_video_command.rs | 0 ...gemagick_generate_preview_image_command.rs | 0 .../src/script_execution/mod.rs | 0 .../wav2lip_process_upload_command.rs | 0 .../{ => job}/w2l_inference_job/Cargo.toml | 16 ++++----- .../{ => job}/w2l_inference_job/src/main.rs | 0 .../src/script_execution/mod.rs | 0 .../wav2lip_inference_command.rs | 0 .../twitch_pubsub_subscriber/Cargo.toml | 14 ++++---- .../twitch_pubsub_subscriber/src/main.rs | 0 .../listen_for_active_obs_sessions_thread.rs | 0 .../listen_for_active_obs_sessions/mod.rs | 0 .../src/threads/mod.rs | 0 .../event_handlers/bits_event_handler.rs | 0 .../channel_points_event_handler.rs | 0 .../event_handlers/mod.rs | 0 .../twitch_pubsub_user_subscriber/mod.rs | 0 .../subscriber_preferences_cache.rs | 0 .../tts_writer.rs | 0 .../twitch_pubsub_user_subscriber_thread.rs | 0 .../src/twitch/constants.rs | 0 .../src/twitch/mod.rs | 0 .../src/twitch/oauth/mod.rs | 0 .../src/twitch/oauth/oauth_token_refresher.rs | 0 .../pubsub/build_pubsub_topics_for_user.rs | 0 .../src/twitch/pubsub/mod.rs | 0 .../src/twitch/websocket_client.rs | 0 .../twitch_pubsub_subscriber/src/util/mod.rs | 0 .../src/util/random_nonce.rs | 0 .../image_generation_service/Cargo.toml | 26 +++++++------- .../image_generation_service/src/main.rs | 0 .../image_generation_service/src/routes.rs | 0 .../src/server_state.rs | 0 .../{ => web}/storyteller_web/Cargo.toml | 36 +++++++++---------- .../internal_product_to_stripe_lookup_impl.rs | 0 .../storyteller_web/src/billing/mod.rs | 0 ...ternal_subscription_product_lookup_impl.rs | 0 .../stripe_internal_user_lookup_impl.rs | 0 .../storyteller_web/src/configs/mod.rs | 0 .../plans/get_correct_plan_for_session.rs | 0 .../storyteller_web/src/configs/plans/mod.rs | 0 .../storyteller_web/src/configs/plans/plan.rs | 0 .../src/configs/plans/plan_category.rs | 0 .../src/configs/plans/plan_list.rs | 0 .../src/configs/static_api_tokens.rs | 0 .../endpoints/api_tokens/create_api_token.rs | 0 .../endpoints/api_tokens/delete_api_token.rs | 0 .../endpoints/api_tokens/edit_api_token.rs | 0 .../endpoints/api_tokens/list_api_tokens.rs | 0 .../http_server/endpoints/api_tokens/mod.rs | 0 .../categories/assign_tts_category.rs | 0 .../endpoints/categories/create_category.rs | 0 .../endpoints/categories/get_category.rs | 0 .../categories/list_tts_categories.rs | 0 .../list_tts_model_assigned_categories.rs | 0 .../http_server/endpoints/categories/mod.rs | 0 .../download_job/enqueue_generic_download.rs | 0 .../get_generic_upload_job_status.rs | 0 .../http_server/endpoints/download_job/mod.rs | 0 .../endpoints/events/list_events.rs | 0 .../src/http_server/endpoints/events/mod.rs | 0 .../build_design_refresh_cookie.rs | 0 .../disable_design_refresh_flag_handler.rs | 0 .../enable_design_refresh_flag_handler.rs | 0 .../flags/design_refresh_flag/mod.rs | 0 .../endpoints/flags/get_cookie_domain.rs | 0 .../src/http_server/endpoints/flags/mod.rs | 0 .../investor_demo/default_redirect.rs | 0 .../endpoints/investor_demo/demo_cookie.rs | 0 .../disable_demo_mode_handler.rs | 0 .../investor_demo/enable_demo_mode_handler.rs | 0 .../endpoints/investor_demo/mod.rs | 0 .../endpoints/leaderboard/get_leaderboard.rs | 0 .../http_server/endpoints/leaderboard/mod.rs | 0 .../endpoints/misc/alpha_cookie.rs | 0 .../endpoints/misc/default_route_404.rs | 0 .../endpoints/misc/detect_locale_handler.rs | 0 .../misc/enable_alpha_easy_handler.rs | 0 .../endpoints/misc/enable_alpha_handler.rs | 0 .../src/http_server/endpoints/misc/mod.rs | 0 .../http_server/endpoints/misc/root_index.rs | 0 .../src/http_server/endpoints/mod.rs | 0 .../endpoints/moderation/approval/mod.rs | 0 .../approval/pending_w2l_templates.rs | 0 .../moderation/categories/delete_category.rs | 0 .../moderation/categories/edit_category.rs | 0 .../list_tts_categories_for_moderation.rs | 0 .../endpoints/moderation/categories/mod.rs | 0 .../moderation/ip_bans/add_ip_ban.rs | 0 .../moderation/ip_bans/delete_ip_ban.rs | 0 .../moderation/ip_bans/get_ip_ban.rs | 0 .../moderation/ip_bans/list_ip_bans.rs | 0 .../endpoints/moderation/ip_bans/mod.rs | 0 .../jobs/get_tts_inference_queue_count.rs | 0 .../jobs/get_w2l_inference_queue_count.rs | 0 .../jobs/kill_tts_inference_jobs.rs | 0 .../endpoints/moderation/jobs/mod.rs | 0 .../http_server/endpoints/moderation/mod.rs | 0 .../stats/get_on_prem_worker_stats.rs | 0 .../moderation/stats/get_voice_count_stats.rs | 0 .../endpoints/moderation/stats/mod.rs | 0 .../moderation/user_bans/ban_user.rs | 0 .../moderation/user_bans/list_banned_users.rs | 0 .../endpoints/moderation/user_bans/mod.rs | 0 .../moderation/user_roles/list_roles.rs | 0 .../moderation/user_roles/list_staff.rs | 0 .../endpoints/moderation/user_roles/mod.rs | 0 .../moderation/user_roles/set_user_role.rs | 0 .../endpoints/moderation/users/list_users.rs | 0 .../endpoints/moderation/users/mod.rs | 0 .../endpoints/service/health_check_handler.rs | 0 .../src/http_server/endpoints/service/mod.rs | 0 .../endpoints/stubs/app_model_downloads.rs | 0 .../http_server/endpoints/stubs/app_news.rs | 0 .../http_server/endpoints/stubs/app_plans.rs | 0 .../src/http_server/endpoints/stubs/mod.rs | 0 .../endpoints/stubs/post_app_analytics.rs | 0 .../endpoints/tts/delete_tts_model.rs | 0 .../endpoints/tts/delete_tts_result.rs | 0 .../endpoints/tts/edit_tts_model.rs | 0 .../endpoints/tts/edit_tts_result.rs | 0 .../endpoints/tts/enqueue_infer_tts.rs | 0 .../endpoints/tts/enqueue_upload_tts_model.rs | 0 .../get_pending_tts_inference_job_count.rs | 0 .../tts/get_tts_inference_job_status.rs | 0 .../endpoints/tts/get_tts_model.rs | 0 .../endpoints/tts/get_tts_model_use_count.rs | 0 .../endpoints/tts/get_tts_result.rs | 0 .../tts/get_tts_upload_model_job_status.rs | 0 .../endpoints/tts/list_tts_models.rs | 0 .../tts/list_user_tts_inference_results.rs | 0 .../endpoints/tts/list_user_tts_models.rs | 0 .../src/http_server/endpoints/tts/mod.rs | 0 .../twitch/event_rules/create_event_rule.rs | 0 .../twitch/event_rules/delete_event_rule.rs | 0 .../twitch/event_rules/edit_event_rule.rs | 0 .../twitch/event_rules/get_event_rule.rs | 0 .../event_rules/list_event_rules_for_user.rs | 0 .../endpoints/twitch/event_rules/mod.rs | 0 .../event_rules/reorder_twitch_event_rules.rs | 0 .../twitch/event_rules/validations/mod.rs | 0 .../validate_event_match_predicate.rs | 0 .../validations/validate_event_response.rs | 0 .../src/http_server/endpoints/twitch/mod.rs | 0 .../twitch/oauth/check_oauth_status.rs | 0 .../http_server/endpoints/twitch/oauth/mod.rs | 0 .../twitch/oauth/oauth_begin_json.rs | 0 .../twitch/oauth/oauth_begin_redirect.rs | 0 .../endpoints/twitch/oauth/oauth_end.rs | 0 .../endpoints/vocoders/delete_vocoder.rs | 0 .../endpoints/vocoders/edit_vocoder.rs | 0 .../endpoints/vocoders/get_vocoder.rs | 0 .../endpoints/vocoders/list_user_vocoders.rs | 0 .../endpoints/vocoders/list_vocoders.rs | 0 .../src/http_server/endpoints/vocoders/mod.rs | 0 .../check_if_voice_clone_request_submitted.rs | 0 .../create_voice_clone_request.rs | 0 .../endpoints/voice_clone_requests/mod.rs | 0 .../endpoints/w2l/delete_w2l_result.rs | 0 .../endpoints/w2l/delete_w2l_template.rs | 0 .../endpoints/w2l/edit_w2l_result.rs | 0 .../endpoints/w2l/edit_w2l_template.rs | 0 .../endpoints/w2l/enqueue_infer_w2l.rs | 0 .../w2l/enqueue_infer_w2l_with_uploads.rs | 0 .../w2l/enqueue_upload_w2l_template.rs | 0 .../w2l/get_w2l_inference_job_status.rs | 0 .../endpoints/w2l/get_w2l_result.rs | 0 .../endpoints/w2l/get_w2l_template.rs | 0 .../w2l/get_w2l_template_use_count.rs | 0 .../w2l/get_w2l_upload_template_job_status.rs | 0 .../w2l/list_user_w2l_inference_results.rs | 0 .../endpoints/w2l/list_user_w2l_templates.rs | 0 .../endpoints/w2l/list_w2l_templates.rs | 0 .../src/http_server/endpoints/w2l/mod.rs | 0 .../w2l/set_w2l_template_mod_approval.rs | 0 .../middleware/ip_filter_middleware.rs | 0 .../src/http_server/middleware/mod.rs | 0 .../storyteller_web/src/http_server/mod.rs | 0 .../http_server/web_utils/get_host_header.rs | 0 .../src/http_server/web_utils/mod.rs | 0 .../web_utils/read_multipart_field_bytes.rs | 0 .../web_utils/redis_rate_limiter.rs | 0 .../web_utils/response_error_helpers.rs | 0 .../web_utils/response_success_helpers.rs | 0 .../web_utils/serialize_as_json_error.rs | 0 .../{ => web}/storyteller_web/src/main.rs | 4 +-- .../{ => web}/storyteller_web/src/routes.rs | 0 .../storyteller_web/src/server_state.rs | 0 .../storyteller_web/src/subscriptions/mod.rs | 0 .../src/subscriptions/subscription_plan.rs | 0 .../subscriptions/subscription_plan_list.rs | 0 .../db_health_check_status.rs | 0 .../db_health_checker_thread.rs | 0 .../threads/db_health_checker_thread/mod.rs | 0 .../src/threads/ip_banlist_set.rs | 0 .../storyteller_web/src/threads/mod.rs | 0 .../src/threads/poll_ip_banlist_thread.rs | 0 .../src/util/delete_role_disambiguation.rs | 0 .../src/util/encrypted_sort_id.rs | 0 .../{ => web}/storyteller_web/src/util/mod.rs | 0 .../storyteller_web/src/validations/mod.rs | 0 .../src/validations/model_uploads.rs | 0 .../validate_idempotency_token_format.rs | 0 .../{ => web}/websocket_gateway/Cargo.toml | 12 +++---- .../src/endpoints_ws/helpers/mod.rs | 0 .../helpers/publish_active_browser_info.rs | 0 .../websocket_gateway/src/endpoints_ws/mod.rs | 0 .../obs_gateway_websocket_handler.rs | 0 .../src/endpoints_ws/threads/mod.rs | 0 .../redis_pubsub_event_listener_thread.rs | 0 .../threads/tts_inference_job_token_queue.rs | 0 .../{ => web}/websocket_gateway/src/main.rs | 0 .../websocket_gateway/src/server_state.rs | 0 280 files changed, 106 insertions(+), 106 deletions(-) rename crates/service/{ => job}/download_job/Cargo.toml (68%) rename crates/service/{ => job}/download_job/src/job_state.rs (100%) rename crates/service/{ => job}/download_job/src/job_steps/job_results.rs (100%) rename crates/service/{ => job}/download_job/src/job_steps/main_loop.rs (100%) rename crates/service/{ => job}/download_job/src/job_steps/mod.rs (100%) rename crates/service/{ => job}/download_job/src/job_steps/process_single_job.rs (100%) rename crates/service/{ => job}/download_job/src/job_types/hifigan/hifigan_model_check_command.rs (100%) rename crates/service/{ => job}/download_job/src/job_types/hifigan/mod.rs (100%) rename crates/service/{ => job}/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs (100%) rename crates/service/{ => job}/download_job/src/job_types/mod.rs (100%) rename crates/service/{ => job}/download_job/src/job_types/tacotron/mod.rs (100%) rename crates/service/{ => job}/download_job/src/job_types/tacotron/process_tacotron_model.rs (100%) rename crates/service/{ => job}/download_job/src/job_types/tacotron/tacotron_model_check_command.rs (100%) rename crates/service/{ => job}/download_job/src/job_types/talknet/mod.rs (100%) rename crates/service/{ => job}/download_job/src/job_types/talknet/talknet_model_check_command.rs (100%) rename crates/service/{ => job}/download_job/src/main.rs (100%) rename crates/service/{ => job}/inference_job/Cargo.toml (73%) rename crates/service/{ => job}/inference_job/src/caching/cache_miss_strategizer.rs (100%) rename crates/service/{ => job}/inference_job/src/caching/cache_miss_strategizer_multi.rs (100%) rename crates/service/{ => job}/inference_job/src/caching/mod.rs (100%) rename crates/service/{ => job}/inference_job/src/caching/virtual_lfu_cache.rs (100%) rename crates/service/{ => job}/inference_job/src/http_clients/mod.rs (100%) rename crates/service/{ => job}/inference_job/src/http_clients/tts_inference_sidecar_client.rs (100%) rename crates/service/{ => job}/inference_job/src/http_clients/tts_sidecar_health_check_client.rs (100%) rename crates/service/{ => job}/inference_job/src/job_steps/download_file_from_bucket.rs (100%) rename crates/service/{ => job}/inference_job/src/job_steps/health_check_trap.rs (100%) rename crates/service/{ => job}/inference_job/src/job_steps/job_args.rs (100%) rename crates/service/{ => job}/inference_job/src/job_steps/job_stats.rs (100%) rename crates/service/{ => job}/inference_job/src/job_steps/mod.rs (100%) rename crates/service/{ => job}/inference_job/src/job_steps/process_single_job.rs (100%) rename crates/service/{ => job}/inference_job/src/job_steps/process_single_job_error.rs (100%) rename crates/service/{ => job}/inference_job/src/job_steps/seconds_to_decoder_steps.rs (100%) rename crates/service/{ => job}/inference_job/src/main.rs (100%) rename crates/service/{ => job}/inference_job/src/script_execution/mod.rs (100%) rename crates/service/{ => job}/inference_job/src/script_execution/tacotron_inference_command.rs (100%) rename crates/service/{ => job}/inference_job/src/util/mod.rs (100%) rename crates/service/{ => job}/inference_job/src/util/scoped_temp_dir_creator.rs (100%) rename crates/service/{ => job}/tts_download_job/Cargo.toml (73%) rename crates/service/{ => job}/tts_download_job/src/main.rs (100%) rename crates/service/{ => job}/tts_download_job/src/script_execution/mod.rs (100%) rename crates/service/{ => job}/tts_download_job/src/script_execution/tacotron_model_check_command.rs (100%) rename crates/service/{ => job}/tts_download_job/src/script_execution/talknet_model_check_command.rs (100%) rename crates/service/{ => job}/tts_inference_job/Cargo.toml (73%) rename crates/service/{ => job}/tts_inference_job/src/caching/cache_miss_strategizer.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/caching/cache_miss_strategizer_multi.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/caching/mod.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/caching/virtual_lfu_cache.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/http_clients/mod.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/http_clients/tts_inference_sidecar_client.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/http_clients/tts_sidecar_health_check_client.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/job_steps/download_file_from_bucket.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/job_steps/health_check_trap.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/job_steps/job_args.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/job_steps/job_stats.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/job_steps/mod.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/job_steps/process_single_job.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/job_steps/process_single_job_error.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/main.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/script_execution/mod.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/script_execution/tacotron_inference_command.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/util/mod.rs (100%) rename crates/service/{ => job}/tts_inference_job/src/util/scoped_temp_dir_creator.rs (100%) rename crates/service/{ => job}/w2l_download_job/Cargo.toml (73%) rename crates/service/{ => job}/w2l_download_job/src/main.rs (100%) rename crates/service/{ => job}/w2l_download_job/src/script_execution/ffmpeg_generate_preview_image_command.rs (100%) rename crates/service/{ => job}/w2l_download_job/src/script_execution/ffmpeg_generate_preview_video_command.rs (100%) rename crates/service/{ => job}/w2l_download_job/src/script_execution/imagemagick_generate_preview_image_command.rs (100%) rename crates/service/{ => job}/w2l_download_job/src/script_execution/mod.rs (100%) rename crates/service/{ => job}/w2l_download_job/src/script_execution/wav2lip_process_upload_command.rs (100%) rename crates/service/{ => job}/w2l_inference_job/Cargo.toml (73%) rename crates/service/{ => job}/w2l_inference_job/src/main.rs (100%) rename crates/service/{ => job}/w2l_inference_job/src/script_execution/mod.rs (100%) rename crates/service/{ => job}/w2l_inference_job/src/script_execution/wav2lip_inference_command.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/Cargo.toml (89%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/main.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/listen_for_active_obs_sessions_thread.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/mod.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/mod.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/bits_event_handler.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/channel_points_event_handler.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/mod.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/mod.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/subscriber_preferences_cache.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/tts_writer.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/twitch_pubsub_user_subscriber_thread.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/twitch/constants.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/twitch/mod.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/twitch/oauth/mod.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/twitch/oauth/oauth_token_refresher.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/twitch/pubsub/build_pubsub_topics_for_user.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/twitch/pubsub/mod.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/twitch/websocket_client.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/util/mod.rs (100%) rename crates/service/{ => other}/twitch_pubsub_subscriber/src/util/random_nonce.rs (100%) rename crates/service/{ => web}/image_generation_service/Cargo.toml (79%) rename crates/service/{ => web}/image_generation_service/src/main.rs (100%) rename crates/service/{ => web}/image_generation_service/src/routes.rs (100%) rename crates/service/{ => web}/image_generation_service/src/server_state.rs (100%) rename crates/service/{ => web}/storyteller_web/Cargo.toml (77%) rename crates/service/{ => web}/storyteller_web/src/billing/internal_product_to_stripe_lookup_impl.rs (100%) rename crates/service/{ => web}/storyteller_web/src/billing/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/billing/stripe_internal_subscription_product_lookup_impl.rs (100%) rename crates/service/{ => web}/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs (100%) rename crates/service/{ => web}/storyteller_web/src/configs/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/configs/plans/get_correct_plan_for_session.rs (100%) rename crates/service/{ => web}/storyteller_web/src/configs/plans/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/configs/plans/plan.rs (100%) rename crates/service/{ => web}/storyteller_web/src/configs/plans/plan_category.rs (100%) rename crates/service/{ => web}/storyteller_web/src/configs/plans/plan_list.rs (100%) rename crates/service/{ => web}/storyteller_web/src/configs/static_api_tokens.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/api_tokens/delete_api_token.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/api_tokens/edit_api_token.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/api_tokens/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/categories/create_category.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/categories/get_category.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/categories/list_tts_model_assigned_categories.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/categories/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/download_job/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/events/list_events.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/events/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/build_design_refresh_cookie.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/disable_design_refresh_flag_handler.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/enable_design_refresh_flag_handler.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/flags/get_cookie_domain.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/flags/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/investor_demo/default_redirect.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/investor_demo/demo_cookie.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/investor_demo/disable_demo_mode_handler.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/investor_demo/enable_demo_mode_handler.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/investor_demo/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/leaderboard/get_leaderboard.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/leaderboard/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/misc/alpha_cookie.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/misc/default_route_404.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/misc/enable_alpha_easy_handler.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/misc/enable_alpha_handler.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/misc/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/misc/root_index.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/approval/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/approval/pending_w2l_templates.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/categories/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/ip_bans/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/jobs/get_tts_inference_queue_count.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/jobs/get_w2l_inference_queue_count.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/jobs/kill_tts_inference_jobs.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/jobs/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/stats/get_on_prem_worker_stats.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/stats/get_voice_count_stats.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/stats/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/user_bans/list_banned_users.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/user_bans/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/user_roles/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/users/list_users.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/moderation/users/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/service/health_check_handler.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/service/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/stubs/app_model_downloads.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/stubs/app_news.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/stubs/app_plans.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/stubs/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/stubs/post_app_analytics.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/delete_tts_model.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/get_pending_tts_inference_job_count.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/get_tts_inference_job_status.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/get_tts_model_use_count.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/get_tts_upload_model_job_status.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/list_tts_models.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/tts/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/create_event_rule.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/delete_event_rule.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/edit_event_rule.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/get_event_rule.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/list_event_rules_for_user.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/reorder_twitch_event_rules.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_match_predicate.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_response.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/oauth/check_oauth_status.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/oauth/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_json.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_redirect.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_end.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/vocoders/delete_vocoder.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/vocoders/edit_vocoder.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/vocoders/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/get_w2l_inference_job_status.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/get_w2l_result.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template_use_count.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/get_w2l_upload_template_job_status.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/list_w2l_templates.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/middleware/ip_filter_middleware.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/middleware/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/web_utils/get_host_header.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/web_utils/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/web_utils/read_multipart_field_bytes.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/web_utils/redis_rate_limiter.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/web_utils/response_error_helpers.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/web_utils/response_success_helpers.rs (100%) rename crates/service/{ => web}/storyteller_web/src/http_server/web_utils/serialize_as_json_error.rs (100%) rename crates/service/{ => web}/storyteller_web/src/main.rs (99%) rename crates/service/{ => web}/storyteller_web/src/routes.rs (100%) rename crates/service/{ => web}/storyteller_web/src/server_state.rs (100%) rename crates/service/{ => web}/storyteller_web/src/subscriptions/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/subscriptions/subscription_plan.rs (100%) rename crates/service/{ => web}/storyteller_web/src/subscriptions/subscription_plan_list.rs (100%) rename crates/service/{ => web}/storyteller_web/src/threads/db_health_checker_thread/db_health_check_status.rs (100%) rename crates/service/{ => web}/storyteller_web/src/threads/db_health_checker_thread/db_health_checker_thread.rs (100%) rename crates/service/{ => web}/storyteller_web/src/threads/db_health_checker_thread/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/threads/ip_banlist_set.rs (100%) rename crates/service/{ => web}/storyteller_web/src/threads/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/threads/poll_ip_banlist_thread.rs (100%) rename crates/service/{ => web}/storyteller_web/src/util/delete_role_disambiguation.rs (100%) rename crates/service/{ => web}/storyteller_web/src/util/encrypted_sort_id.rs (100%) rename crates/service/{ => web}/storyteller_web/src/util/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/validations/mod.rs (100%) rename crates/service/{ => web}/storyteller_web/src/validations/model_uploads.rs (100%) rename crates/service/{ => web}/storyteller_web/src/validations/validate_idempotency_token_format.rs (100%) rename crates/service/{ => web}/websocket_gateway/Cargo.toml (90%) rename crates/service/{ => web}/websocket_gateway/src/endpoints_ws/helpers/mod.rs (100%) rename crates/service/{ => web}/websocket_gateway/src/endpoints_ws/helpers/publish_active_browser_info.rs (100%) rename crates/service/{ => web}/websocket_gateway/src/endpoints_ws/mod.rs (100%) rename crates/service/{ => web}/websocket_gateway/src/endpoints_ws/obs_gateway_websocket_handler.rs (100%) rename crates/service/{ => web}/websocket_gateway/src/endpoints_ws/threads/mod.rs (100%) rename crates/service/{ => web}/websocket_gateway/src/endpoints_ws/threads/redis_pubsub_event_listener_thread.rs (100%) rename crates/service/{ => web}/websocket_gateway/src/endpoints_ws/threads/tts_inference_job_token_queue.rs (100%) rename crates/service/{ => web}/websocket_gateway/src/main.rs (100%) rename crates/service/{ => web}/websocket_gateway/src/server_state.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index af9d86a146..ac4ddb632a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,14 +29,14 @@ members = [ "crates/lib/user_input_common", # Services: API servers, jobs, workers, etc. - "crates/service/download_job", - "crates/service/image_generation_service", - "crates/service/inference_job", - "crates/service/storyteller_web", - "crates/service/tts_download_job", - "crates/service/tts_inference_job", - "crates/service/twitch_pubsub_subscriber", - "crates/service/w2l_download_job", - "crates/service/w2l_inference_job", - "crates/service/websocket_gateway", + "crates/service/job/download_job", + "crates/service/job/inference_job", + "crates/service/job/tts_download_job", + "crates/service/job/tts_inference_job", + "crates/service/job/w2l_download_job", + "crates/service/job/w2l_inference_job", + "crates/service/other/twitch_pubsub_subscriber", + "crates/service/web/image_generation_service", + "crates/service/web/storyteller_web", + "crates/service/web/websocket_gateway", ] diff --git a/crates/service/download_job/Cargo.toml b/crates/service/job/download_job/Cargo.toml similarity index 68% rename from crates/service/download_job/Cargo.toml rename to crates/service/job/download_job/Cargo.toml index 7de6324f9c..bd0becbfd6 100644 --- a/crates/service/download_job/Cargo.toml +++ b/crates/service/job/download_job/Cargo.toml @@ -15,18 +15,18 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -jobs_common = { path = "../../lib/jobs_common" } -reusable_types = { path = "../../lib/reusable_types" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -subprocess_common = { path = "../../lib/subprocess_common" } -tts_common = { path = "../../lib/tts_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +jobs_common = { path = "../../../lib/jobs_common" } +reusable_types = { path = "../../../lib/reusable_types" } +storage_buckets_common = { path = "../../../lib/storage_buckets_common" } +subprocess_common = { path = "../../../lib/subprocess_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/download_job/src/job_state.rs b/crates/service/job/download_job/src/job_state.rs similarity index 100% rename from crates/service/download_job/src/job_state.rs rename to crates/service/job/download_job/src/job_state.rs diff --git a/crates/service/download_job/src/job_steps/job_results.rs b/crates/service/job/download_job/src/job_steps/job_results.rs similarity index 100% rename from crates/service/download_job/src/job_steps/job_results.rs rename to crates/service/job/download_job/src/job_steps/job_results.rs diff --git a/crates/service/download_job/src/job_steps/main_loop.rs b/crates/service/job/download_job/src/job_steps/main_loop.rs similarity index 100% rename from crates/service/download_job/src/job_steps/main_loop.rs rename to crates/service/job/download_job/src/job_steps/main_loop.rs diff --git a/crates/service/download_job/src/job_steps/mod.rs b/crates/service/job/download_job/src/job_steps/mod.rs similarity index 100% rename from crates/service/download_job/src/job_steps/mod.rs rename to crates/service/job/download_job/src/job_steps/mod.rs diff --git a/crates/service/download_job/src/job_steps/process_single_job.rs b/crates/service/job/download_job/src/job_steps/process_single_job.rs similarity index 100% rename from crates/service/download_job/src/job_steps/process_single_job.rs rename to crates/service/job/download_job/src/job_steps/process_single_job.rs diff --git a/crates/service/download_job/src/job_types/hifigan/hifigan_model_check_command.rs b/crates/service/job/download_job/src/job_types/hifigan/hifigan_model_check_command.rs similarity index 100% rename from crates/service/download_job/src/job_types/hifigan/hifigan_model_check_command.rs rename to crates/service/job/download_job/src/job_types/hifigan/hifigan_model_check_command.rs diff --git a/crates/service/download_job/src/job_types/hifigan/mod.rs b/crates/service/job/download_job/src/job_types/hifigan/mod.rs similarity index 100% rename from crates/service/download_job/src/job_types/hifigan/mod.rs rename to crates/service/job/download_job/src/job_types/hifigan/mod.rs diff --git a/crates/service/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs b/crates/service/job/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs similarity index 100% rename from crates/service/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs rename to crates/service/job/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs diff --git a/crates/service/download_job/src/job_types/mod.rs b/crates/service/job/download_job/src/job_types/mod.rs similarity index 100% rename from crates/service/download_job/src/job_types/mod.rs rename to crates/service/job/download_job/src/job_types/mod.rs diff --git a/crates/service/download_job/src/job_types/tacotron/mod.rs b/crates/service/job/download_job/src/job_types/tacotron/mod.rs similarity index 100% rename from crates/service/download_job/src/job_types/tacotron/mod.rs rename to crates/service/job/download_job/src/job_types/tacotron/mod.rs diff --git a/crates/service/download_job/src/job_types/tacotron/process_tacotron_model.rs b/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs similarity index 100% rename from crates/service/download_job/src/job_types/tacotron/process_tacotron_model.rs rename to crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs diff --git a/crates/service/download_job/src/job_types/tacotron/tacotron_model_check_command.rs b/crates/service/job/download_job/src/job_types/tacotron/tacotron_model_check_command.rs similarity index 100% rename from crates/service/download_job/src/job_types/tacotron/tacotron_model_check_command.rs rename to crates/service/job/download_job/src/job_types/tacotron/tacotron_model_check_command.rs diff --git a/crates/service/download_job/src/job_types/talknet/mod.rs b/crates/service/job/download_job/src/job_types/talknet/mod.rs similarity index 100% rename from crates/service/download_job/src/job_types/talknet/mod.rs rename to crates/service/job/download_job/src/job_types/talknet/mod.rs diff --git a/crates/service/download_job/src/job_types/talknet/talknet_model_check_command.rs b/crates/service/job/download_job/src/job_types/talknet/talknet_model_check_command.rs similarity index 100% rename from crates/service/download_job/src/job_types/talknet/talknet_model_check_command.rs rename to crates/service/job/download_job/src/job_types/talknet/talknet_model_check_command.rs diff --git a/crates/service/download_job/src/main.rs b/crates/service/job/download_job/src/main.rs similarity index 100% rename from crates/service/download_job/src/main.rs rename to crates/service/job/download_job/src/main.rs diff --git a/crates/service/inference_job/Cargo.toml b/crates/service/job/inference_job/Cargo.toml similarity index 73% rename from crates/service/inference_job/Cargo.toml rename to crates/service/job/inference_job/Cargo.toml index 5919936220..9366e14ddb 100644 --- a/crates/service/inference_job/Cargo.toml +++ b/crates/service/job/inference_job/Cargo.toml @@ -15,16 +15,16 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -jobs_common = { path = "../../lib/jobs_common" } -memory_caching = { path = "../../lib/memory_caching" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +jobs_common = { path = "../../../lib/jobs_common" } +memory_caching = { path = "../../../lib/memory_caching" } +storage_buckets_common = { path = "../../../lib/storage_buckets_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/inference_job/src/caching/cache_miss_strategizer.rs b/crates/service/job/inference_job/src/caching/cache_miss_strategizer.rs similarity index 100% rename from crates/service/inference_job/src/caching/cache_miss_strategizer.rs rename to crates/service/job/inference_job/src/caching/cache_miss_strategizer.rs diff --git a/crates/service/inference_job/src/caching/cache_miss_strategizer_multi.rs b/crates/service/job/inference_job/src/caching/cache_miss_strategizer_multi.rs similarity index 100% rename from crates/service/inference_job/src/caching/cache_miss_strategizer_multi.rs rename to crates/service/job/inference_job/src/caching/cache_miss_strategizer_multi.rs diff --git a/crates/service/inference_job/src/caching/mod.rs b/crates/service/job/inference_job/src/caching/mod.rs similarity index 100% rename from crates/service/inference_job/src/caching/mod.rs rename to crates/service/job/inference_job/src/caching/mod.rs diff --git a/crates/service/inference_job/src/caching/virtual_lfu_cache.rs b/crates/service/job/inference_job/src/caching/virtual_lfu_cache.rs similarity index 100% rename from crates/service/inference_job/src/caching/virtual_lfu_cache.rs rename to crates/service/job/inference_job/src/caching/virtual_lfu_cache.rs diff --git a/crates/service/inference_job/src/http_clients/mod.rs b/crates/service/job/inference_job/src/http_clients/mod.rs similarity index 100% rename from crates/service/inference_job/src/http_clients/mod.rs rename to crates/service/job/inference_job/src/http_clients/mod.rs diff --git a/crates/service/inference_job/src/http_clients/tts_inference_sidecar_client.rs b/crates/service/job/inference_job/src/http_clients/tts_inference_sidecar_client.rs similarity index 100% rename from crates/service/inference_job/src/http_clients/tts_inference_sidecar_client.rs rename to crates/service/job/inference_job/src/http_clients/tts_inference_sidecar_client.rs diff --git a/crates/service/inference_job/src/http_clients/tts_sidecar_health_check_client.rs b/crates/service/job/inference_job/src/http_clients/tts_sidecar_health_check_client.rs similarity index 100% rename from crates/service/inference_job/src/http_clients/tts_sidecar_health_check_client.rs rename to crates/service/job/inference_job/src/http_clients/tts_sidecar_health_check_client.rs diff --git a/crates/service/inference_job/src/job_steps/download_file_from_bucket.rs b/crates/service/job/inference_job/src/job_steps/download_file_from_bucket.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/download_file_from_bucket.rs rename to crates/service/job/inference_job/src/job_steps/download_file_from_bucket.rs diff --git a/crates/service/inference_job/src/job_steps/health_check_trap.rs b/crates/service/job/inference_job/src/job_steps/health_check_trap.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/health_check_trap.rs rename to crates/service/job/inference_job/src/job_steps/health_check_trap.rs diff --git a/crates/service/inference_job/src/job_steps/job_args.rs b/crates/service/job/inference_job/src/job_steps/job_args.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/job_args.rs rename to crates/service/job/inference_job/src/job_steps/job_args.rs diff --git a/crates/service/inference_job/src/job_steps/job_stats.rs b/crates/service/job/inference_job/src/job_steps/job_stats.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/job_stats.rs rename to crates/service/job/inference_job/src/job_steps/job_stats.rs diff --git a/crates/service/inference_job/src/job_steps/mod.rs b/crates/service/job/inference_job/src/job_steps/mod.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/mod.rs rename to crates/service/job/inference_job/src/job_steps/mod.rs diff --git a/crates/service/inference_job/src/job_steps/process_single_job.rs b/crates/service/job/inference_job/src/job_steps/process_single_job.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/process_single_job.rs rename to crates/service/job/inference_job/src/job_steps/process_single_job.rs diff --git a/crates/service/inference_job/src/job_steps/process_single_job_error.rs b/crates/service/job/inference_job/src/job_steps/process_single_job_error.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/process_single_job_error.rs rename to crates/service/job/inference_job/src/job_steps/process_single_job_error.rs diff --git a/crates/service/inference_job/src/job_steps/seconds_to_decoder_steps.rs b/crates/service/job/inference_job/src/job_steps/seconds_to_decoder_steps.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/seconds_to_decoder_steps.rs rename to crates/service/job/inference_job/src/job_steps/seconds_to_decoder_steps.rs diff --git a/crates/service/inference_job/src/main.rs b/crates/service/job/inference_job/src/main.rs similarity index 100% rename from crates/service/inference_job/src/main.rs rename to crates/service/job/inference_job/src/main.rs diff --git a/crates/service/inference_job/src/script_execution/mod.rs b/crates/service/job/inference_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/inference_job/src/script_execution/mod.rs rename to crates/service/job/inference_job/src/script_execution/mod.rs diff --git a/crates/service/inference_job/src/script_execution/tacotron_inference_command.rs b/crates/service/job/inference_job/src/script_execution/tacotron_inference_command.rs similarity index 100% rename from crates/service/inference_job/src/script_execution/tacotron_inference_command.rs rename to crates/service/job/inference_job/src/script_execution/tacotron_inference_command.rs diff --git a/crates/service/inference_job/src/util/mod.rs b/crates/service/job/inference_job/src/util/mod.rs similarity index 100% rename from crates/service/inference_job/src/util/mod.rs rename to crates/service/job/inference_job/src/util/mod.rs diff --git a/crates/service/inference_job/src/util/scoped_temp_dir_creator.rs b/crates/service/job/inference_job/src/util/scoped_temp_dir_creator.rs similarity index 100% rename from crates/service/inference_job/src/util/scoped_temp_dir_creator.rs rename to crates/service/job/inference_job/src/util/scoped_temp_dir_creator.rs diff --git a/crates/service/tts_download_job/Cargo.toml b/crates/service/job/tts_download_job/Cargo.toml similarity index 73% rename from crates/service/tts_download_job/Cargo.toml rename to crates/service/job/tts_download_job/Cargo.toml index c0acba04e4..a7e08183e7 100644 --- a/crates/service/tts_download_job/Cargo.toml +++ b/crates/service/job/tts_download_job/Cargo.toml @@ -15,16 +15,16 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -jobs_common = { path = "../../lib/jobs_common" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +jobs_common = { path = "../../../lib/jobs_common" } +storage_buckets_common = { path = "../../../lib/storage_buckets_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/tts_download_job/src/main.rs b/crates/service/job/tts_download_job/src/main.rs similarity index 100% rename from crates/service/tts_download_job/src/main.rs rename to crates/service/job/tts_download_job/src/main.rs diff --git a/crates/service/tts_download_job/src/script_execution/mod.rs b/crates/service/job/tts_download_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/tts_download_job/src/script_execution/mod.rs rename to crates/service/job/tts_download_job/src/script_execution/mod.rs diff --git a/crates/service/tts_download_job/src/script_execution/tacotron_model_check_command.rs b/crates/service/job/tts_download_job/src/script_execution/tacotron_model_check_command.rs similarity index 100% rename from crates/service/tts_download_job/src/script_execution/tacotron_model_check_command.rs rename to crates/service/job/tts_download_job/src/script_execution/tacotron_model_check_command.rs diff --git a/crates/service/tts_download_job/src/script_execution/talknet_model_check_command.rs b/crates/service/job/tts_download_job/src/script_execution/talknet_model_check_command.rs similarity index 100% rename from crates/service/tts_download_job/src/script_execution/talknet_model_check_command.rs rename to crates/service/job/tts_download_job/src/script_execution/talknet_model_check_command.rs diff --git a/crates/service/tts_inference_job/Cargo.toml b/crates/service/job/tts_inference_job/Cargo.toml similarity index 73% rename from crates/service/tts_inference_job/Cargo.toml rename to crates/service/job/tts_inference_job/Cargo.toml index 62d22f4060..6982193089 100644 --- a/crates/service/tts_inference_job/Cargo.toml +++ b/crates/service/job/tts_inference_job/Cargo.toml @@ -15,16 +15,16 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -jobs_common = { path = "../../lib/jobs_common" } -memory_caching = { path = "../../lib/memory_caching" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +jobs_common = { path = "../../../lib/jobs_common" } +memory_caching = { path = "../../../lib/memory_caching" } +storage_buckets_common = { path = "../../../lib/storage_buckets_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/tts_inference_job/src/caching/cache_miss_strategizer.rs b/crates/service/job/tts_inference_job/src/caching/cache_miss_strategizer.rs similarity index 100% rename from crates/service/tts_inference_job/src/caching/cache_miss_strategizer.rs rename to crates/service/job/tts_inference_job/src/caching/cache_miss_strategizer.rs diff --git a/crates/service/tts_inference_job/src/caching/cache_miss_strategizer_multi.rs b/crates/service/job/tts_inference_job/src/caching/cache_miss_strategizer_multi.rs similarity index 100% rename from crates/service/tts_inference_job/src/caching/cache_miss_strategizer_multi.rs rename to crates/service/job/tts_inference_job/src/caching/cache_miss_strategizer_multi.rs diff --git a/crates/service/tts_inference_job/src/caching/mod.rs b/crates/service/job/tts_inference_job/src/caching/mod.rs similarity index 100% rename from crates/service/tts_inference_job/src/caching/mod.rs rename to crates/service/job/tts_inference_job/src/caching/mod.rs diff --git a/crates/service/tts_inference_job/src/caching/virtual_lfu_cache.rs b/crates/service/job/tts_inference_job/src/caching/virtual_lfu_cache.rs similarity index 100% rename from crates/service/tts_inference_job/src/caching/virtual_lfu_cache.rs rename to crates/service/job/tts_inference_job/src/caching/virtual_lfu_cache.rs diff --git a/crates/service/tts_inference_job/src/http_clients/mod.rs b/crates/service/job/tts_inference_job/src/http_clients/mod.rs similarity index 100% rename from crates/service/tts_inference_job/src/http_clients/mod.rs rename to crates/service/job/tts_inference_job/src/http_clients/mod.rs diff --git a/crates/service/tts_inference_job/src/http_clients/tts_inference_sidecar_client.rs b/crates/service/job/tts_inference_job/src/http_clients/tts_inference_sidecar_client.rs similarity index 100% rename from crates/service/tts_inference_job/src/http_clients/tts_inference_sidecar_client.rs rename to crates/service/job/tts_inference_job/src/http_clients/tts_inference_sidecar_client.rs diff --git a/crates/service/tts_inference_job/src/http_clients/tts_sidecar_health_check_client.rs b/crates/service/job/tts_inference_job/src/http_clients/tts_sidecar_health_check_client.rs similarity index 100% rename from crates/service/tts_inference_job/src/http_clients/tts_sidecar_health_check_client.rs rename to crates/service/job/tts_inference_job/src/http_clients/tts_sidecar_health_check_client.rs diff --git a/crates/service/tts_inference_job/src/job_steps/download_file_from_bucket.rs b/crates/service/job/tts_inference_job/src/job_steps/download_file_from_bucket.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/download_file_from_bucket.rs rename to crates/service/job/tts_inference_job/src/job_steps/download_file_from_bucket.rs diff --git a/crates/service/tts_inference_job/src/job_steps/health_check_trap.rs b/crates/service/job/tts_inference_job/src/job_steps/health_check_trap.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/health_check_trap.rs rename to crates/service/job/tts_inference_job/src/job_steps/health_check_trap.rs diff --git a/crates/service/tts_inference_job/src/job_steps/job_args.rs b/crates/service/job/tts_inference_job/src/job_steps/job_args.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/job_args.rs rename to crates/service/job/tts_inference_job/src/job_steps/job_args.rs diff --git a/crates/service/tts_inference_job/src/job_steps/job_stats.rs b/crates/service/job/tts_inference_job/src/job_steps/job_stats.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/job_stats.rs rename to crates/service/job/tts_inference_job/src/job_steps/job_stats.rs diff --git a/crates/service/tts_inference_job/src/job_steps/mod.rs b/crates/service/job/tts_inference_job/src/job_steps/mod.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/mod.rs rename to crates/service/job/tts_inference_job/src/job_steps/mod.rs diff --git a/crates/service/tts_inference_job/src/job_steps/process_single_job.rs b/crates/service/job/tts_inference_job/src/job_steps/process_single_job.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/process_single_job.rs rename to crates/service/job/tts_inference_job/src/job_steps/process_single_job.rs diff --git a/crates/service/tts_inference_job/src/job_steps/process_single_job_error.rs b/crates/service/job/tts_inference_job/src/job_steps/process_single_job_error.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/process_single_job_error.rs rename to crates/service/job/tts_inference_job/src/job_steps/process_single_job_error.rs diff --git a/crates/service/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs b/crates/service/job/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs rename to crates/service/job/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs diff --git a/crates/service/tts_inference_job/src/main.rs b/crates/service/job/tts_inference_job/src/main.rs similarity index 100% rename from crates/service/tts_inference_job/src/main.rs rename to crates/service/job/tts_inference_job/src/main.rs diff --git a/crates/service/tts_inference_job/src/script_execution/mod.rs b/crates/service/job/tts_inference_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/tts_inference_job/src/script_execution/mod.rs rename to crates/service/job/tts_inference_job/src/script_execution/mod.rs diff --git a/crates/service/tts_inference_job/src/script_execution/tacotron_inference_command.rs b/crates/service/job/tts_inference_job/src/script_execution/tacotron_inference_command.rs similarity index 100% rename from crates/service/tts_inference_job/src/script_execution/tacotron_inference_command.rs rename to crates/service/job/tts_inference_job/src/script_execution/tacotron_inference_command.rs diff --git a/crates/service/tts_inference_job/src/util/mod.rs b/crates/service/job/tts_inference_job/src/util/mod.rs similarity index 100% rename from crates/service/tts_inference_job/src/util/mod.rs rename to crates/service/job/tts_inference_job/src/util/mod.rs diff --git a/crates/service/tts_inference_job/src/util/scoped_temp_dir_creator.rs b/crates/service/job/tts_inference_job/src/util/scoped_temp_dir_creator.rs similarity index 100% rename from crates/service/tts_inference_job/src/util/scoped_temp_dir_creator.rs rename to crates/service/job/tts_inference_job/src/util/scoped_temp_dir_creator.rs diff --git a/crates/service/w2l_download_job/Cargo.toml b/crates/service/job/w2l_download_job/Cargo.toml similarity index 73% rename from crates/service/w2l_download_job/Cargo.toml rename to crates/service/job/w2l_download_job/Cargo.toml index 2b0dd5df88..072be21166 100644 --- a/crates/service/w2l_download_job/Cargo.toml +++ b/crates/service/job/w2l_download_job/Cargo.toml @@ -15,16 +15,16 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -jobs_common = { path = "../../lib/jobs_common" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +jobs_common = { path = "../../../lib/jobs_common" } +storage_buckets_common = { path = "../../../lib/storage_buckets_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/w2l_download_job/src/main.rs b/crates/service/job/w2l_download_job/src/main.rs similarity index 100% rename from crates/service/w2l_download_job/src/main.rs rename to crates/service/job/w2l_download_job/src/main.rs diff --git a/crates/service/w2l_download_job/src/script_execution/ffmpeg_generate_preview_image_command.rs b/crates/service/job/w2l_download_job/src/script_execution/ffmpeg_generate_preview_image_command.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/ffmpeg_generate_preview_image_command.rs rename to crates/service/job/w2l_download_job/src/script_execution/ffmpeg_generate_preview_image_command.rs diff --git a/crates/service/w2l_download_job/src/script_execution/ffmpeg_generate_preview_video_command.rs b/crates/service/job/w2l_download_job/src/script_execution/ffmpeg_generate_preview_video_command.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/ffmpeg_generate_preview_video_command.rs rename to crates/service/job/w2l_download_job/src/script_execution/ffmpeg_generate_preview_video_command.rs diff --git a/crates/service/w2l_download_job/src/script_execution/imagemagick_generate_preview_image_command.rs b/crates/service/job/w2l_download_job/src/script_execution/imagemagick_generate_preview_image_command.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/imagemagick_generate_preview_image_command.rs rename to crates/service/job/w2l_download_job/src/script_execution/imagemagick_generate_preview_image_command.rs diff --git a/crates/service/w2l_download_job/src/script_execution/mod.rs b/crates/service/job/w2l_download_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/mod.rs rename to crates/service/job/w2l_download_job/src/script_execution/mod.rs diff --git a/crates/service/w2l_download_job/src/script_execution/wav2lip_process_upload_command.rs b/crates/service/job/w2l_download_job/src/script_execution/wav2lip_process_upload_command.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/wav2lip_process_upload_command.rs rename to crates/service/job/w2l_download_job/src/script_execution/wav2lip_process_upload_command.rs diff --git a/crates/service/w2l_inference_job/Cargo.toml b/crates/service/job/w2l_inference_job/Cargo.toml similarity index 73% rename from crates/service/w2l_inference_job/Cargo.toml rename to crates/service/job/w2l_inference_job/Cargo.toml index 6b8f26ab50..a127dbac92 100644 --- a/crates/service/w2l_inference_job/Cargo.toml +++ b/crates/service/job/w2l_inference_job/Cargo.toml @@ -15,16 +15,16 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -jobs_common = { path = "../../lib/jobs_common" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +jobs_common = { path = "../../../lib/jobs_common" } +storage_buckets_common = { path = "../../../lib/storage_buckets_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/w2l_inference_job/src/main.rs b/crates/service/job/w2l_inference_job/src/main.rs similarity index 100% rename from crates/service/w2l_inference_job/src/main.rs rename to crates/service/job/w2l_inference_job/src/main.rs diff --git a/crates/service/w2l_inference_job/src/script_execution/mod.rs b/crates/service/job/w2l_inference_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/w2l_inference_job/src/script_execution/mod.rs rename to crates/service/job/w2l_inference_job/src/script_execution/mod.rs diff --git a/crates/service/w2l_inference_job/src/script_execution/wav2lip_inference_command.rs b/crates/service/job/w2l_inference_job/src/script_execution/wav2lip_inference_command.rs similarity index 100% rename from crates/service/w2l_inference_job/src/script_execution/wav2lip_inference_command.rs rename to crates/service/job/w2l_inference_job/src/script_execution/wav2lip_inference_command.rs diff --git a/crates/service/twitch_pubsub_subscriber/Cargo.toml b/crates/service/other/twitch_pubsub_subscriber/Cargo.toml similarity index 89% rename from crates/service/twitch_pubsub_subscriber/Cargo.toml rename to crates/service/other/twitch_pubsub_subscriber/Cargo.toml index 8e154cf384..03c1ca872c 100644 --- a/crates/service/twitch_pubsub_subscriber/Cargo.toml +++ b/crates/service/other/twitch_pubsub_subscriber/Cargo.toml @@ -15,13 +15,13 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -http_server_common = { path = "../../lib/http_server_common" } -redis_common = { path = "../../lib/redis_common" } -tts_common = { path = "../../lib/tts_common" } -twitch_common = { path = "../../lib/twitch_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +http_server_common = { path = "../../../lib/http_server_common" } +redis_common = { path = "../../../lib/redis_common" } +tts_common = { path = "../../../lib/tts_common" } +twitch_common = { path = "../../../lib/twitch_common" } # External anyhow = "1.0.40" diff --git a/crates/service/twitch_pubsub_subscriber/src/main.rs b/crates/service/other/twitch_pubsub_subscriber/src/main.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/main.rs rename to crates/service/other/twitch_pubsub_subscriber/src/main.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/listen_for_active_obs_sessions_thread.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/listen_for_active_obs_sessions_thread.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/listen_for_active_obs_sessions_thread.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/listen_for_active_obs_sessions_thread.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/bits_event_handler.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/bits_event_handler.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/bits_event_handler.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/bits_event_handler.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/channel_points_event_handler.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/channel_points_event_handler.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/channel_points_event_handler.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/channel_points_event_handler.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/subscriber_preferences_cache.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/subscriber_preferences_cache.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/subscriber_preferences_cache.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/subscriber_preferences_cache.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/tts_writer.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/tts_writer.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/tts_writer.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/tts_writer.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/twitch_pubsub_user_subscriber_thread.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/twitch_pubsub_user_subscriber_thread.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/twitch_pubsub_user_subscriber_thread.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/twitch_pubsub_user_subscriber_thread.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/constants.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/constants.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/constants.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/constants.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/oauth/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/oauth/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/oauth/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/oauth/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/oauth/oauth_token_refresher.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/oauth/oauth_token_refresher.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/oauth/oauth_token_refresher.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/oauth/oauth_token_refresher.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/pubsub/build_pubsub_topics_for_user.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/pubsub/build_pubsub_topics_for_user.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/pubsub/build_pubsub_topics_for_user.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/pubsub/build_pubsub_topics_for_user.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/pubsub/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/pubsub/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/pubsub/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/pubsub/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/websocket_client.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/websocket_client.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/websocket_client.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/websocket_client.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/util/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/util/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/util/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/util/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/util/random_nonce.rs b/crates/service/other/twitch_pubsub_subscriber/src/util/random_nonce.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/util/random_nonce.rs rename to crates/service/other/twitch_pubsub_subscriber/src/util/random_nonce.rs diff --git a/crates/service/image_generation_service/Cargo.toml b/crates/service/web/image_generation_service/Cargo.toml similarity index 79% rename from crates/service/image_generation_service/Cargo.toml rename to crates/service/web/image_generation_service/Cargo.toml index 31c08b374b..4b64b2c8e7 100644 --- a/crates/service/image_generation_service/Cargo.toml +++ b/crates/service/web/image_generation_service/Cargo.toml @@ -16,19 +16,19 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -http_server_common = { path = "../../lib/http_server_common" } -jobs_common = { path = "../../lib/jobs_common" } -memory_caching = { path = "../../lib/memory_caching" } -redis_common = { path = "../../lib/redis_common" } -reusable_types = { path = "../../lib/reusable_types" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } -twitch_common = { path = "../../lib/twitch_common" } -user_input_common = { path = "../../lib/user_input_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +http_server_common = { path = "../../../lib/http_server_common" } +jobs_common = { path = "../../../lib/jobs_common" } +memory_caching = { path = "../../../lib/memory_caching" } +redis_common = { path = "../../../lib/redis_common" } +reusable_types = { path = "../../../lib/reusable_types" } +storage_buckets_common = { path = "../../../lib/storage_buckets_common" } +tts_common = { path = "../../../lib/tts_common" } +twitch_common = { path = "../../../lib/twitch_common" } +user_input_common = { path = "../../../lib/user_input_common" } # External anyhow = "1.0.40" diff --git a/crates/service/image_generation_service/src/main.rs b/crates/service/web/image_generation_service/src/main.rs similarity index 100% rename from crates/service/image_generation_service/src/main.rs rename to crates/service/web/image_generation_service/src/main.rs diff --git a/crates/service/image_generation_service/src/routes.rs b/crates/service/web/image_generation_service/src/routes.rs similarity index 100% rename from crates/service/image_generation_service/src/routes.rs rename to crates/service/web/image_generation_service/src/routes.rs diff --git a/crates/service/image_generation_service/src/server_state.rs b/crates/service/web/image_generation_service/src/server_state.rs similarity index 100% rename from crates/service/image_generation_service/src/server_state.rs rename to crates/service/web/image_generation_service/src/server_state.rs diff --git a/crates/service/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml similarity index 77% rename from crates/service/storyteller_web/Cargo.toml rename to crates/service/web/storyteller_web/Cargo.toml index 5cc9db3bf3..2d31a374f3 100644 --- a/crates/service/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -16,28 +16,28 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -http_server_common = { path = "../../lib/http_server_common" } -jobs_common = { path = "../../lib/jobs_common" } -memory_caching = { path = "../../lib/memory_caching" } -redis_common = { path = "../../lib/redis_common" } -reusable_types = { path = "../../lib/reusable_types" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tokens = { path = "../../lib/tokens" } -tts_common = { path = "../../lib/tts_common" } -twitch_common = { path = "../../lib/twitch_common" } -url_config = { path = "../../lib/url_config" } -user_input_common = { path = "../../lib/user_input_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +http_server_common = { path = "../../../lib/http_server_common" } +jobs_common = { path = "../../../lib/jobs_common" } +memory_caching = { path = "../../../lib/memory_caching" } +redis_common = { path = "../../../lib/redis_common" } +reusable_types = { path = "../../../lib/reusable_types" } +storage_buckets_common = { path = "../../../lib/storage_buckets_common" } +tokens = { path = "../../../lib/tokens" } +tts_common = { path = "../../../lib/tts_common" } +twitch_common = { path = "../../../lib/twitch_common" } +url_config = { path = "../../../lib/url_config" } +user_input_common = { path = "../../../lib/user_input_common" } # Internal Components -billing_component = { path = "../../plugins/billing" } -users_component = { path = "../../plugins/users" } +billing_component = { path = "../../../plugins/billing" } +users_component = { path = "../../../plugins/users" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/storyteller_web/src/billing/internal_product_to_stripe_lookup_impl.rs b/crates/service/web/storyteller_web/src/billing/internal_product_to_stripe_lookup_impl.rs similarity index 100% rename from crates/service/storyteller_web/src/billing/internal_product_to_stripe_lookup_impl.rs rename to crates/service/web/storyteller_web/src/billing/internal_product_to_stripe_lookup_impl.rs diff --git a/crates/service/storyteller_web/src/billing/mod.rs b/crates/service/web/storyteller_web/src/billing/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/billing/mod.rs rename to crates/service/web/storyteller_web/src/billing/mod.rs diff --git a/crates/service/storyteller_web/src/billing/stripe_internal_subscription_product_lookup_impl.rs b/crates/service/web/storyteller_web/src/billing/stripe_internal_subscription_product_lookup_impl.rs similarity index 100% rename from crates/service/storyteller_web/src/billing/stripe_internal_subscription_product_lookup_impl.rs rename to crates/service/web/storyteller_web/src/billing/stripe_internal_subscription_product_lookup_impl.rs diff --git a/crates/service/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs b/crates/service/web/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs similarity index 100% rename from crates/service/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs rename to crates/service/web/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs diff --git a/crates/service/storyteller_web/src/configs/mod.rs b/crates/service/web/storyteller_web/src/configs/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/mod.rs rename to crates/service/web/storyteller_web/src/configs/mod.rs diff --git a/crates/service/storyteller_web/src/configs/plans/get_correct_plan_for_session.rs b/crates/service/web/storyteller_web/src/configs/plans/get_correct_plan_for_session.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/plans/get_correct_plan_for_session.rs rename to crates/service/web/storyteller_web/src/configs/plans/get_correct_plan_for_session.rs diff --git a/crates/service/storyteller_web/src/configs/plans/mod.rs b/crates/service/web/storyteller_web/src/configs/plans/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/plans/mod.rs rename to crates/service/web/storyteller_web/src/configs/plans/mod.rs diff --git a/crates/service/storyteller_web/src/configs/plans/plan.rs b/crates/service/web/storyteller_web/src/configs/plans/plan.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/plans/plan.rs rename to crates/service/web/storyteller_web/src/configs/plans/plan.rs diff --git a/crates/service/storyteller_web/src/configs/plans/plan_category.rs b/crates/service/web/storyteller_web/src/configs/plans/plan_category.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/plans/plan_category.rs rename to crates/service/web/storyteller_web/src/configs/plans/plan_category.rs diff --git a/crates/service/storyteller_web/src/configs/plans/plan_list.rs b/crates/service/web/storyteller_web/src/configs/plans/plan_list.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/plans/plan_list.rs rename to crates/service/web/storyteller_web/src/configs/plans/plan_list.rs diff --git a/crates/service/storyteller_web/src/configs/static_api_tokens.rs b/crates/service/web/storyteller_web/src/configs/static_api_tokens.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/static_api_tokens.rs rename to crates/service/web/storyteller_web/src/configs/static_api_tokens.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/delete_api_token.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/delete_api_token.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/delete_api_token.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/delete_api_token.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/edit_api_token.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/edit_api_token.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/edit_api_token.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/edit_api_token.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/create_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/create_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/create_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/create_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/get_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/get_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/get_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/get_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/list_tts_model_assigned_categories.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/list_tts_model_assigned_categories.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/list_tts_model_assigned_categories.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/list_tts_model_assigned_categories.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/download_job/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/download_job/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/download_job/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/events/list_events.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/events/list_events.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/events/list_events.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/events/list_events.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/events/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/events/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/events/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/events/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/build_design_refresh_cookie.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/build_design_refresh_cookie.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/build_design_refresh_cookie.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/build_design_refresh_cookie.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/disable_design_refresh_flag_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/disable_design_refresh_flag_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/disable_design_refresh_flag_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/disable_design_refresh_flag_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/enable_design_refresh_flag_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/enable_design_refresh_flag_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/enable_design_refresh_flag_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/enable_design_refresh_flag_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/get_cookie_domain.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/get_cookie_domain.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/get_cookie_domain.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/get_cookie_domain.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/default_redirect.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/default_redirect.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/default_redirect.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/default_redirect.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/demo_cookie.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/demo_cookie.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/demo_cookie.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/demo_cookie.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/disable_demo_mode_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/disable_demo_mode_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/disable_demo_mode_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/disable_demo_mode_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/enable_demo_mode_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/enable_demo_mode_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/enable_demo_mode_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/enable_demo_mode_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/leaderboard/get_leaderboard.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/leaderboard/get_leaderboard.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/leaderboard/get_leaderboard.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/leaderboard/get_leaderboard.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/leaderboard/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/leaderboard/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/leaderboard/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/leaderboard/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/alpha_cookie.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/alpha_cookie.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/alpha_cookie.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/alpha_cookie.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/default_route_404.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/default_route_404.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/default_route_404.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/default_route_404.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/enable_alpha_easy_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/enable_alpha_easy_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/enable_alpha_easy_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/enable_alpha_easy_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/enable_alpha_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/enable_alpha_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/enable_alpha_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/enable_alpha_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/root_index.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/root_index.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/root_index.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/root_index.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/approval/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/approval/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/approval/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/approval/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/approval/pending_w2l_templates.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/approval/pending_w2l_templates.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/approval/pending_w2l_templates.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/approval/pending_w2l_templates.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/get_tts_inference_queue_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/get_tts_inference_queue_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/get_tts_inference_queue_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/get_tts_inference_queue_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/get_w2l_inference_queue_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/get_w2l_inference_queue_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/get_w2l_inference_queue_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/get_w2l_inference_queue_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/kill_tts_inference_jobs.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/kill_tts_inference_jobs.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/kill_tts_inference_jobs.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/kill_tts_inference_jobs.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/get_on_prem_worker_stats.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/get_on_prem_worker_stats.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/get_on_prem_worker_stats.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/get_on_prem_worker_stats.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/get_voice_count_stats.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/get_voice_count_stats.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/get_voice_count_stats.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/get_voice_count_stats.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/list_banned_users.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/list_banned_users.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/list_banned_users.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/list_banned_users.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/users/list_users.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/users/list_users.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/users/list_users.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/users/list_users.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/users/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/users/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/users/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/users/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/service/health_check_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/service/health_check_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/service/health_check_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/service/health_check_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/service/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/service/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/service/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/service/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/app_model_downloads.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_model_downloads.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/app_model_downloads.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_model_downloads.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/app_news.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_news.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/app_news.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_news.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/app_plans.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_plans.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/app_plans.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_plans.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/post_app_analytics.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/post_app_analytics.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/post_app_analytics.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/post_app_analytics.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/delete_tts_model.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_model.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/delete_tts_model.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_pending_tts_inference_job_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_pending_tts_inference_job_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_pending_tts_inference_job_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_pending_tts_inference_job_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_inference_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_inference_job_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_inference_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_inference_job_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_model_use_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model_use_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_model_use_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model_use_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_upload_model_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_upload_model_job_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_upload_model_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_upload_model_job_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/list_tts_models.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_tts_models.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/list_tts_models.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_tts_models.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/create_event_rule.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/create_event_rule.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/create_event_rule.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/create_event_rule.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/delete_event_rule.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/delete_event_rule.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/delete_event_rule.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/delete_event_rule.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/edit_event_rule.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/edit_event_rule.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/edit_event_rule.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/edit_event_rule.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/get_event_rule.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/get_event_rule.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/get_event_rule.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/get_event_rule.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/list_event_rules_for_user.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/list_event_rules_for_user.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/list_event_rules_for_user.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/list_event_rules_for_user.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/reorder_twitch_event_rules.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/reorder_twitch_event_rules.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/reorder_twitch_event_rules.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/reorder_twitch_event_rules.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_match_predicate.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_match_predicate.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_match_predicate.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_match_predicate.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_response.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_response.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_response.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_response.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/check_oauth_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/check_oauth_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/check_oauth_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/check_oauth_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_json.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_json.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_json.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_json.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_redirect.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_redirect.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_redirect.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_redirect.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_end.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_end.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_end.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_end.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/delete_vocoder.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/delete_vocoder.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/delete_vocoder.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/delete_vocoder.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/edit_vocoder.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/edit_vocoder.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/edit_vocoder.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/edit_vocoder.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_inference_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_inference_job_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_inference_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_inference_job_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template_use_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template_use_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template_use_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template_use_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_upload_template_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_upload_template_job_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_upload_template_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_upload_template_job_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/list_w2l_templates.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_w2l_templates.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/list_w2l_templates.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_w2l_templates.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs diff --git a/crates/service/storyteller_web/src/http_server/middleware/ip_filter_middleware.rs b/crates/service/web/storyteller_web/src/http_server/middleware/ip_filter_middleware.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/middleware/ip_filter_middleware.rs rename to crates/service/web/storyteller_web/src/http_server/middleware/ip_filter_middleware.rs diff --git a/crates/service/storyteller_web/src/http_server/middleware/mod.rs b/crates/service/web/storyteller_web/src/http_server/middleware/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/middleware/mod.rs rename to crates/service/web/storyteller_web/src/http_server/middleware/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/mod.rs b/crates/service/web/storyteller_web/src/http_server/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/mod.rs rename to crates/service/web/storyteller_web/src/http_server/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/get_host_header.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/get_host_header.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/get_host_header.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/get_host_header.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/mod.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/mod.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/read_multipart_field_bytes.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/read_multipart_field_bytes.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/read_multipart_field_bytes.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/read_multipart_field_bytes.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/redis_rate_limiter.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/redis_rate_limiter.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/redis_rate_limiter.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/redis_rate_limiter.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/response_error_helpers.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/response_error_helpers.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/response_error_helpers.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/response_error_helpers.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/response_success_helpers.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/response_success_helpers.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/response_success_helpers.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/response_success_helpers.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/serialize_as_json_error.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/serialize_as_json_error.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/serialize_as_json_error.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/serialize_as_json_error.rs diff --git a/crates/service/storyteller_web/src/main.rs b/crates/service/web/storyteller_web/src/main.rs similarity index 99% rename from crates/service/storyteller_web/src/main.rs rename to crates/service/web/storyteller_web/src/main.rs index 8398c8bd22..8540bb754f 100644 --- a/crates/service/storyteller_web/src/main.rs +++ b/crates/service/web/storyteller_web/src/main.rs @@ -17,8 +17,8 @@ #[macro_use] extern crate magic_crypt; #[macro_use] extern crate serde_derive; -pub const RESERVED_USERNAMES : &'static str = include_str!("../../../../db/reserved_usernames.txt"); -pub const RESERVED_SUBSTRINGS : &'static str = include_str!("../../../../db/reserved_usernames_including.txt"); +pub const RESERVED_USERNAMES : &'static str = include_str!("../../../../../db/reserved_usernames.txt"); +pub const RESERVED_SUBSTRINGS : &'static str = include_str!("../../../../../db/reserved_usernames_including.txt"); pub mod billing; pub mod configs; diff --git a/crates/service/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs similarity index 100% rename from crates/service/storyteller_web/src/routes.rs rename to crates/service/web/storyteller_web/src/routes.rs diff --git a/crates/service/storyteller_web/src/server_state.rs b/crates/service/web/storyteller_web/src/server_state.rs similarity index 100% rename from crates/service/storyteller_web/src/server_state.rs rename to crates/service/web/storyteller_web/src/server_state.rs diff --git a/crates/service/storyteller_web/src/subscriptions/mod.rs b/crates/service/web/storyteller_web/src/subscriptions/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/subscriptions/mod.rs rename to crates/service/web/storyteller_web/src/subscriptions/mod.rs diff --git a/crates/service/storyteller_web/src/subscriptions/subscription_plan.rs b/crates/service/web/storyteller_web/src/subscriptions/subscription_plan.rs similarity index 100% rename from crates/service/storyteller_web/src/subscriptions/subscription_plan.rs rename to crates/service/web/storyteller_web/src/subscriptions/subscription_plan.rs diff --git a/crates/service/storyteller_web/src/subscriptions/subscription_plan_list.rs b/crates/service/web/storyteller_web/src/subscriptions/subscription_plan_list.rs similarity index 100% rename from crates/service/storyteller_web/src/subscriptions/subscription_plan_list.rs rename to crates/service/web/storyteller_web/src/subscriptions/subscription_plan_list.rs diff --git a/crates/service/storyteller_web/src/threads/db_health_checker_thread/db_health_check_status.rs b/crates/service/web/storyteller_web/src/threads/db_health_checker_thread/db_health_check_status.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/db_health_checker_thread/db_health_check_status.rs rename to crates/service/web/storyteller_web/src/threads/db_health_checker_thread/db_health_check_status.rs diff --git a/crates/service/storyteller_web/src/threads/db_health_checker_thread/db_health_checker_thread.rs b/crates/service/web/storyteller_web/src/threads/db_health_checker_thread/db_health_checker_thread.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/db_health_checker_thread/db_health_checker_thread.rs rename to crates/service/web/storyteller_web/src/threads/db_health_checker_thread/db_health_checker_thread.rs diff --git a/crates/service/storyteller_web/src/threads/db_health_checker_thread/mod.rs b/crates/service/web/storyteller_web/src/threads/db_health_checker_thread/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/db_health_checker_thread/mod.rs rename to crates/service/web/storyteller_web/src/threads/db_health_checker_thread/mod.rs diff --git a/crates/service/storyteller_web/src/threads/ip_banlist_set.rs b/crates/service/web/storyteller_web/src/threads/ip_banlist_set.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/ip_banlist_set.rs rename to crates/service/web/storyteller_web/src/threads/ip_banlist_set.rs diff --git a/crates/service/storyteller_web/src/threads/mod.rs b/crates/service/web/storyteller_web/src/threads/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/mod.rs rename to crates/service/web/storyteller_web/src/threads/mod.rs diff --git a/crates/service/storyteller_web/src/threads/poll_ip_banlist_thread.rs b/crates/service/web/storyteller_web/src/threads/poll_ip_banlist_thread.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/poll_ip_banlist_thread.rs rename to crates/service/web/storyteller_web/src/threads/poll_ip_banlist_thread.rs diff --git a/crates/service/storyteller_web/src/util/delete_role_disambiguation.rs b/crates/service/web/storyteller_web/src/util/delete_role_disambiguation.rs similarity index 100% rename from crates/service/storyteller_web/src/util/delete_role_disambiguation.rs rename to crates/service/web/storyteller_web/src/util/delete_role_disambiguation.rs diff --git a/crates/service/storyteller_web/src/util/encrypted_sort_id.rs b/crates/service/web/storyteller_web/src/util/encrypted_sort_id.rs similarity index 100% rename from crates/service/storyteller_web/src/util/encrypted_sort_id.rs rename to crates/service/web/storyteller_web/src/util/encrypted_sort_id.rs diff --git a/crates/service/storyteller_web/src/util/mod.rs b/crates/service/web/storyteller_web/src/util/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/util/mod.rs rename to crates/service/web/storyteller_web/src/util/mod.rs diff --git a/crates/service/storyteller_web/src/validations/mod.rs b/crates/service/web/storyteller_web/src/validations/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/validations/mod.rs rename to crates/service/web/storyteller_web/src/validations/mod.rs diff --git a/crates/service/storyteller_web/src/validations/model_uploads.rs b/crates/service/web/storyteller_web/src/validations/model_uploads.rs similarity index 100% rename from crates/service/storyteller_web/src/validations/model_uploads.rs rename to crates/service/web/storyteller_web/src/validations/model_uploads.rs diff --git a/crates/service/storyteller_web/src/validations/validate_idempotency_token_format.rs b/crates/service/web/storyteller_web/src/validations/validate_idempotency_token_format.rs similarity index 100% rename from crates/service/storyteller_web/src/validations/validate_idempotency_token_format.rs rename to crates/service/web/storyteller_web/src/validations/validate_idempotency_token_format.rs diff --git a/crates/service/websocket_gateway/Cargo.toml b/crates/service/web/websocket_gateway/Cargo.toml similarity index 90% rename from crates/service/websocket_gateway/Cargo.toml rename to crates/service/web/websocket_gateway/Cargo.toml index f3debcbe34..c71157c28c 100644 --- a/crates/service/websocket_gateway/Cargo.toml +++ b/crates/service/web/websocket_gateway/Cargo.toml @@ -15,12 +15,12 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -http_server_common = { path = "../../lib/http_server_common" } -redis_common = { path = "../../lib/redis_common" } -twitch_common = { path = "../../lib/twitch_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +http_server_common = { path = "../../../lib/http_server_common" } +redis_common = { path = "../../../lib/redis_common" } +twitch_common = { path = "../../../lib/twitch_common" } # External anyhow = "1.0.40" diff --git a/crates/service/websocket_gateway/src/endpoints_ws/helpers/mod.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/helpers/mod.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/helpers/mod.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/helpers/mod.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/helpers/publish_active_browser_info.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/helpers/publish_active_browser_info.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/helpers/publish_active_browser_info.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/helpers/publish_active_browser_info.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/mod.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/mod.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/mod.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/mod.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/obs_gateway_websocket_handler.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/obs_gateway_websocket_handler.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/obs_gateway_websocket_handler.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/obs_gateway_websocket_handler.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/threads/mod.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/threads/mod.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/threads/mod.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/threads/mod.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/threads/redis_pubsub_event_listener_thread.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/threads/redis_pubsub_event_listener_thread.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/threads/redis_pubsub_event_listener_thread.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/threads/redis_pubsub_event_listener_thread.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/threads/tts_inference_job_token_queue.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/threads/tts_inference_job_token_queue.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/threads/tts_inference_job_token_queue.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/threads/tts_inference_job_token_queue.rs diff --git a/crates/service/websocket_gateway/src/main.rs b/crates/service/web/websocket_gateway/src/main.rs similarity index 100% rename from crates/service/websocket_gateway/src/main.rs rename to crates/service/web/websocket_gateway/src/main.rs diff --git a/crates/service/websocket_gateway/src/server_state.rs b/crates/service/web/websocket_gateway/src/server_state.rs similarity index 100% rename from crates/service/websocket_gateway/src/server_state.rs rename to crates/service/web/websocket_gateway/src/server_state.rs From 02bf8808d0b734af3ed8373d8346eab738026354 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 15 Dec 2022 00:45:47 -0500 Subject: [PATCH 03/54] Web + Job queries for generic_inference_jobs --- .../up.sql | 4 +- .../src/queries/generic_download/mod.rs | 3 +- .../get_generic_download_job_status.rs | 0 .../{ => web}/insert_generic_download_job.rs | 0 .../src/queries/generic_download/web/mod.rs | 2 + .../queries/generic_inference/job/_keys.rs | 6 + .../list_available_generic_inference_jobs.rs | 152 ++++++++++++++++++ ...generic_inference_job_completely_failed.rs | 34 ++++ .../job/mark_generic_inference_job_failure.rs | 46 ++++++ ...ric_inference_job_pending_and_grab_lock.rs | 84 ++++++++++ ...generic_inference_job_successfully_done.rs | 36 +++++ .../src/queries/generic_inference/job/mod.rs | 6 + .../src/queries/generic_inference/mod.rs | 2 + .../web/get_generic_inference_job_status.rs | 61 +++++++ .../web/insert_generic_inference_job.rs | 70 ++++++++ .../src/queries/generic_inference/web/mod.rs | 2 + .../lib/database_queries/src/queries/mod.rs | 1 + .../src/generic_inference_type.rs | 57 +++++++ crates/lib/reusable_types/src/lib.rs | 1 + .../download_job/enqueue_generic_download.rs | 2 +- .../get_generic_upload_job_status.rs | 2 +- 21 files changed, 565 insertions(+), 6 deletions(-) rename crates/lib/database_queries/src/queries/generic_download/{ => web}/get_generic_download_job_status.rs (100%) rename crates/lib/database_queries/src/queries/generic_download/{ => web}/insert_generic_download_job.rs (100%) create mode 100644 crates/lib/database_queries/src/queries/generic_download/web/mod.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/job/_keys.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_completely_failed.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_failure.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_pending_and_grab_lock.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_successfully_done.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/job/mod.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/mod.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs create mode 100644 crates/lib/database_queries/src/queries/generic_inference/web/mod.rs create mode 100644 crates/lib/reusable_types/src/generic_inference_type.rs diff --git a/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql b/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql index b215894dfe..b547482249 100644 --- a/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql +++ b/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql @@ -82,7 +82,7 @@ CREATE TABLE generic_inference_jobs ( -- - All jobs from logged in FakeYou users have level 1. -- - All jobs from Twitch TTS (unpaid) have level 10 (ten). -- - (There will be future levels for paid Twitch and social FakeYou rewards.) - priority_level TINYINT UNSIGNED NOT NULL DEFAULT 0, + priority_level SMALLINT UNSIGNED NOT NULL DEFAULT 0, is_from_premium_user BOOLEAN NOT NULL DEFAULT FALSE, is_from_api_user BOOLEAN NOT NULL DEFAULT FALSE, @@ -114,7 +114,7 @@ CREATE TABLE generic_inference_jobs ( 'dead') NOT NULL DEFAULT 'pending', -- We can track this against a "max_attempt_count" - attempt_count INT(3) NOT NULL DEFAULT 0, + attempt_count SMALLINT UNSIGNED NOT NULL DEFAULT 0, -- If there is a failure, tell the user why. failure_reason VARCHAR(512) DEFAULT NULL, diff --git a/crates/lib/database_queries/src/queries/generic_download/mod.rs b/crates/lib/database_queries/src/queries/generic_download/mod.rs index 4feea997bc..d16f215da7 100644 --- a/crates/lib/database_queries/src/queries/generic_download/mod.rs +++ b/crates/lib/database_queries/src/queries/generic_download/mod.rs @@ -1,3 +1,2 @@ -pub mod get_generic_download_job_status; -pub mod insert_generic_download_job; pub mod job; +pub mod web; diff --git a/crates/lib/database_queries/src/queries/generic_download/get_generic_download_job_status.rs b/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs similarity index 100% rename from crates/lib/database_queries/src/queries/generic_download/get_generic_download_job_status.rs rename to crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs diff --git a/crates/lib/database_queries/src/queries/generic_download/insert_generic_download_job.rs b/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs similarity index 100% rename from crates/lib/database_queries/src/queries/generic_download/insert_generic_download_job.rs rename to crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs diff --git a/crates/lib/database_queries/src/queries/generic_download/web/mod.rs b/crates/lib/database_queries/src/queries/generic_download/web/mod.rs new file mode 100644 index 0000000000..4def6e37b1 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_download/web/mod.rs @@ -0,0 +1,2 @@ +pub mod get_generic_download_job_status; +pub mod insert_generic_download_job; diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/_keys.rs b/crates/lib/database_queries/src/queries/generic_inference/job/_keys.rs new file mode 100644 index 0000000000..93ba2f37e5 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/_keys.rs @@ -0,0 +1,6 @@ +//! Primary and foreign keys + +/// Since jobs are internal, we'll use IDs as primary keys. +#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize, sqlx::Type)] +#[sqlx(transparent)] +pub struct GenericInferenceJobId(pub i64); diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs b/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs new file mode 100644 index 0000000000..fdd5e2bebc --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs @@ -0,0 +1,152 @@ +use anyhow::anyhow; +use chrono::Utc; +use container_common::anyhow_result::AnyhowResult; +use crate::column_types::job_status::JobStatus; +use crate::helpers::boolean_converters::i8_to_bool; +use crate::queries::generic_inference::job::_keys::GenericInferenceJobId; +use reusable_types::entity_visibility::EntityVisibility; +use reusable_types::generic_inference_type::GenericInferenceType; +use sqlx::MySqlPool; +use std::path::Path; +use tokens::jobs::inference::InferenceJobToken; + +/// table: generic_inference_jobs +#[derive(Debug)] +pub struct AvailableInferenceJob { + pub id: GenericInferenceJobId, + pub inference_job_token: InferenceJobToken, + + // Inference information + pub inference_type: GenericInferenceType, + pub maybe_inference_args: Option, + pub maybe_raw_inference_text: Option, + pub maybe_model_token: Option, + + // User information to propagate downstream + pub maybe_creator_user_token: Option, + pub creator_ip_address: String, + pub creator_set_visibility: EntityVisibility, + + // Job information + pub status: JobStatus, + pub attempt_count: u16, + pub priority_level: u16, + pub is_from_premium_user: bool, + pub is_from_api_user: bool, + pub is_for_twitch: bool, + pub is_debug_request: bool, + + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub retry_at: Option>, +} + +pub async fn list_available_generic_inference_jobs(pool: &MySqlPool, num_records: u32) + -> AnyhowResult> +{ + let job_records = sqlx::query_as!( + AvailableInferenceJobRawInternal, + r#" +SELECT + id as `id: crate::queries::generic_inference::job::_keys::GenericInferenceJobId`, + token AS `inference_job_token: tokens::jobs::inference::InferenceJobToken`, + + inference_type as `inference_type: reusable_types::generic_inference_type::GenericInferenceType`, + maybe_inference_args, + maybe_raw_inference_text, + maybe_model_token, + + maybe_creator_user_token, + creator_ip_address, + creator_set_visibility as `creator_set_visibility: reusable_types::entity_visibility::EntityVisibility`, + + status as `status: crate::column_types::job_status::JobStatus`, + + + attempt_count, + priority_level, + is_from_premium_user, + is_from_api_user, + is_for_twitch, + is_debug_request, + + created_at, + updated_at, + retry_at +FROM generic_inference_jobs +WHERE + ( + status IN ("pending", "attempt_failed") + ) + AND + ( + retry_at IS NULL + OR + retry_at < CURRENT_TIMESTAMP + ) + ORDER BY id ASC + LIMIT ? + "#, + num_records, + ) + .fetch_all(pool) + .await?; + + let job_records = job_records.into_iter() + .map(|record : AvailableInferenceJobRawInternal| { + AvailableInferenceJob { + id: record.id, + inference_job_token: record.inference_job_token, + creator_ip_address: record.creator_ip_address, + maybe_creator_user_token: record.maybe_creator_user_token, + creator_set_visibility: record.creator_set_visibility, + inference_type: record.inference_type, + maybe_inference_args: record.maybe_inference_args, + maybe_raw_inference_text: record.maybe_raw_inference_text, + maybe_model_token: record.maybe_model_token, + status: record.status, + attempt_count: record.attempt_count, + priority_level: record.priority_level, + is_from_premium_user: i8_to_bool(record.is_from_premium_user), + is_from_api_user: i8_to_bool(record.is_from_api_user), + is_for_twitch: i8_to_bool(record.is_for_twitch), + is_debug_request: i8_to_bool(record.is_debug_request), + created_at: record.created_at, + updated_at: record.updated_at, + retry_at: record.retry_at, + } + }) + .collect::>(); + + Ok(job_records) +} + +#[derive(Debug)] +struct AvailableInferenceJobRawInternal { + pub id: GenericInferenceJobId, + pub inference_job_token: InferenceJobToken, + + // Inference information + pub inference_type: GenericInferenceType, + pub maybe_inference_args: Option, + pub maybe_raw_inference_text: Option, + pub maybe_model_token: Option, + + // User information to propagate downstream + pub maybe_creator_user_token: Option, + pub creator_ip_address: String, + pub creator_set_visibility: EntityVisibility, + + // Job information + pub status: JobStatus, + pub attempt_count: u16, + pub priority_level: u16, + pub is_from_premium_user: i8, + pub is_from_api_user: i8, + pub is_for_twitch: i8, + pub is_debug_request: i8, + + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub retry_at: Option>, +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_completely_failed.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_completely_failed.rs new file mode 100644 index 0000000000..8ee46fd3de --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_completely_failed.rs @@ -0,0 +1,34 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob; +use sqlx::MySqlPool; + +pub async fn mark_generic_inference_job_completely_failed( + pool: &MySqlPool, + job: &AvailableInferenceJob, + maybe_public_failure_reason: Option<&str>, + maybe_internal_debugging_failure_reason: Option<&str>, +) -> AnyhowResult<()> +{ + let query_result = sqlx::query!( + r#" +UPDATE generic_inference_jobs +SET + status = "complete_failure", + failure_reason = ?, + internal_debugging_failure_reason = ?, + retry_at = NULL +WHERE id = ? + "#, + maybe_public_failure_reason, + maybe_internal_debugging_failure_reason, + job.id.0, + ) + .execute(pool) + .await; + + match query_result { + Err(err) => Err(anyhow!("error with query: {:?}", err)), + Ok(_r) => Ok(()), + } +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_failure.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_failure.rs new file mode 100644 index 0000000000..aa33c88462 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_failure.rs @@ -0,0 +1,46 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob; +use sqlx::MySqlPool; +use sqlx; + +/// Mark a single inference job failure. The job may be re-run. +pub async fn mark_generic_inference_job_failure( + pool: &MySqlPool, + job: &AvailableInferenceJob, + failure_reason: &str, + internal_debugging_failure_reason: &str, + max_attempts: u16 +) -> AnyhowResult<()> { + + // statuses: "attempt_failed", "complete_failure", "dead" + let mut next_status = "attempt_failed"; + + if job.attempt_count >= max_attempts { + // NB: Job attempt count is incremented at start + next_status = "dead"; + } + + let query_result = sqlx::query!( + r#" +UPDATE generic_inference_jobs +SET + status = ?, + failure_reason = ?, + internal_debugging_failure_reason = ?, + retry_at = NOW() + interval 2 minute +WHERE id = ? + "#, + next_status, + failure_reason, + internal_debugging_failure_reason, + job.id.0, + ) + .execute(pool) + .await; + + match query_result { + Err(err) => Err(anyhow!("error with query: {:?}", err)), + Ok(_r) => Ok(()), + } +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_pending_and_grab_lock.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_pending_and_grab_lock.rs new file mode 100644 index 0000000000..ace9494698 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_pending_and_grab_lock.rs @@ -0,0 +1,84 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use crate::queries::generic_inference::job::_keys::GenericInferenceJobId; +use sqlx::MySqlPool; +use sqlx; + +// TODO(2022-02-09): It's unclear how stuck "pending" jobs don't become perma-dead +// I think this logic is flawed and the job system needs to pick up stuck "pending" jobs. + +pub struct GenericInferenceJobLockRecord { + id: i64, + status: String, +} + +pub async fn mark_generic_inference_job_pending_and_grab_lock( + pool: &MySqlPool, + job_id: GenericInferenceJobId +) -> AnyhowResult { + + // NB: We use transactions and "SELECT ... FOR UPDATE" to simulate mutexes. + let mut transaction = pool.begin().await?; + + let maybe_record = sqlx::query_as!( + GenericInferenceJobLockRecord, + r#" +SELECT + id, + status +FROM generic_inference_jobs +WHERE id = ? +FOR UPDATE + "#, + job_id.0, + ) + .fetch_one(&mut transaction) + .await; + + let record : GenericInferenceJobLockRecord = match maybe_record { + Ok(record) => record, + Err(err) => { + match err { + sqlx::Error::RowNotFound => { + return Err(anyhow!("could not job")); + }, + _ => { + return Err(anyhow!("query error")); + } + } + } + }; + + let can_transact = match record.status.as_ref() { + "pending" => true, // It's okay for us to take the lock. + "attempt_failed" => true, // We can retry. + "started" => false, // Job in progress (another job beat us, and we can't take the lock) + "complete_success" => false, // Job already complete + "complete_failure" => false, // Job already complete (permanently dead; no need to retry) + "dead" => false, // Job already complete (permanently dead; retries exhausted) + _ => false, // Future-proof + }; + + if !can_transact { + transaction.rollback().await?; + return Ok(false); + } + + let _acquire_lock = sqlx::query!( + r#" +UPDATE generic_inference_jobs +SET + status = 'started', + attempt_count = attempt_count + 1, + retry_at = NOW() + interval 2 minute +WHERE id = ? + "#, + job_id.0, + ) + .execute(&mut transaction) + .await?; + + transaction.commit().await?; + + Ok(true) +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_successfully_done.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_successfully_done.rs new file mode 100644 index 0000000000..1fbeec22cf --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_successfully_done.rs @@ -0,0 +1,36 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob; +use sqlx::MySqlPool; + +pub async fn mark_generic_inference_job_successfully_done( + pool: &MySqlPool, + job: &AvailableInferenceJob, + maybe_entity_type: Option<&str>, + maybe_entity_token: Option<&str>, +) -> AnyhowResult<()> +{ + let query_result = sqlx::query!( + r#" +UPDATE generic_inference_jobs +SET + status = "complete_success", + on_success_result_entity_type = ?, + on_success_result_entity_token = ?, + failure_reason = NULL, + internal_debugging_failure_reason = NULL, + retry_at = NULL +WHERE id = ? + "#, + maybe_entity_type, + maybe_entity_token, + job.id.0, + ) + .execute(pool) + .await; + + match query_result { + Err(err) => Err(anyhow!("error with query: {:?}", err)), + Ok(_r) => Ok(()), + } +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mod.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mod.rs new file mode 100644 index 0000000000..2a4c4e14a6 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mod.rs @@ -0,0 +1,6 @@ +pub mod _keys; +pub mod list_available_generic_inference_jobs; +pub mod mark_generic_inference_job_completely_failed; +pub mod mark_generic_inference_job_successfully_done; +pub mod mark_generic_inference_job_failure; +pub mod mark_generic_inference_job_pending_and_grab_lock; diff --git a/crates/lib/database_queries/src/queries/generic_inference/mod.rs b/crates/lib/database_queries/src/queries/generic_inference/mod.rs new file mode 100644 index 0000000000..d16f215da7 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/mod.rs @@ -0,0 +1,2 @@ +pub mod job; +pub mod web; diff --git a/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs b/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs new file mode 100644 index 0000000000..7e16eb03ef --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs @@ -0,0 +1,61 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use container_common::anyhow_result::AnyhowResult; +use log::warn; +use reusable_types::entity_visibility::EntityVisibility; +use reusable_types::generic_inference_type::GenericInferenceType; +use sqlx::MySqlPool; +use tokens::jobs::inference::InferenceJobToken; + +pub struct GenericInferenceJobStatus { + pub job_token: InferenceJobToken, + + pub status: String, + pub attempt_count: u16, + + pub maybe_result_entity_type: Option, + pub maybe_result_entity_token: Option, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// Look up job status. +/// Returns Ok(None) when the record cannot be found. +pub async fn get_generic_inference_job_status(job_token: &InferenceJobToken, mysql_pool: &MySqlPool) + -> AnyhowResult> +{ + let maybe_status = sqlx::query_as!( + GenericInferenceJobStatus, + r#" +SELECT + jobs.token as `job_token: tokens::jobs::inference::InferenceJobToken`, + + jobs.status, + jobs.attempt_count, + jobs.on_success_result_entity_type as maybe_result_entity_type, + jobs.on_success_result_entity_token as maybe_result_entity_token, + + jobs.created_at, + jobs.updated_at + +FROM generic_inference_jobs as jobs + +WHERE jobs.token = ? + "#, + job_token + ) + .fetch_one(mysql_pool) + .await; + + match maybe_status { + Ok(record) => Ok(Some(record)), + Err(err) => match err { + sqlx::Error::RowNotFound => Ok(None), + _ => { + warn!("error querying job record: {:?}", err); + Err(anyhow!("error querying job record: {:?}", err)) + } + } + } +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs b/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs new file mode 100644 index 0000000000..fceee79e7a --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs @@ -0,0 +1,70 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use reusable_types::entity_visibility::EntityVisibility; +use reusable_types::generic_inference_type::GenericInferenceType; +use sqlx::MySqlPool; +use tokens::jobs::inference::InferenceJobToken; + +pub struct Args <'a> { + pub job_token: &'a InferenceJobToken, + pub uuid_idempotency_token: &'a str, + + pub inference_type: GenericInferenceType, + pub maybe_inference_args: Option, // TODO: Enum struct type + pub maybe_raw_inference_text: Option, + pub maybe_model_token: Option, + + pub maybe_creator_user_token: Option<&'a str>, + pub creator_ip_address: &'a str, + pub creator_set_visibility: EntityVisibility, + + pub mysql_pool: &'a MySqlPool, +} + +pub async fn insert_generic_inference_job(args: Args<'_>) -> AnyhowResult { + + let query = sqlx::query!( + r#" +INSERT INTO generic_inference_jobs +SET + token = ?, + uuid_idempotency_token = ?, + + inference_type = ?, + maybe_inference_args = ?, + maybe_raw_inference_text = ?, + maybe_model_token = ?, + + maybe_creator_user_token = ?, + creator_ip_address = ?, + creator_set_visibility = ?, + + status = "pending" + "#, + args.job_token, + args.uuid_idempotency_token, + + args.inference_type, + args.maybe_inference_args, + args.maybe_raw_inference_text, + args.maybe_model_token, + + args.maybe_creator_user_token, + args.creator_ip_address, + args.creator_set_visibility.to_str(), + ); + + let query_result = query.execute(args.mysql_pool) + .await; + + let record_id = match query_result { + Ok(res) => { + res.last_insert_id() + }, + Err(err) => { + return Err(anyhow!("error inserting new generic inference job: {:?}", err)); + } + }; + + Ok(record_id) +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/web/mod.rs b/crates/lib/database_queries/src/queries/generic_inference/web/mod.rs new file mode 100644 index 0000000000..3e4d73ced2 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/web/mod.rs @@ -0,0 +1,2 @@ +pub mod get_generic_inference_job_status; +pub mod insert_generic_inference_job; diff --git a/crates/lib/database_queries/src/queries/mod.rs b/crates/lib/database_queries/src/queries/mod.rs index 57ffb02a8e..8f072bfb32 100644 --- a/crates/lib/database_queries/src/queries/mod.rs +++ b/crates/lib/database_queries/src/queries/mod.rs @@ -1,6 +1,7 @@ pub mod api_tokens; pub mod billing; pub mod generic_download; +pub mod generic_inference; pub mod health_check; pub mod ip_bans; pub mod model_categories; diff --git a/crates/lib/reusable_types/src/generic_inference_type.rs b/crates/lib/reusable_types/src/generic_inference_type.rs new file mode 100644 index 0000000000..084fed383d --- /dev/null +++ b/crates/lib/reusable_types/src/generic_inference_type.rs @@ -0,0 +1,57 @@ +/// Our "generic inference" pipeline supports a wide variety of ML models and other media. +/// Each type of inference is identified by the following enum variants. +/// These types are present in the HTTP API and database columns as serialized here. +/// +/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] +pub enum GenericInferenceType { + #[serde(rename = "text_to_speech")] + #[sqlx(rename = "text_to_speech")] + TextToSpeech, + + #[serde(rename = "voice_conversion")] + #[sqlx(rename = "voice_conversion")] + VoiceConversion, +} + +/// NB: Legacy API for older code. +impl GenericInferenceType { + pub fn to_str(&self) -> &'static str { + match self { + Self::TextToSpeech => "text_to_speech", + Self::VoiceConversion => "voice_conversion", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "text_to_speech" => Ok(Self::TextToSpeech), + "voice_conversion" => Ok(Self::VoiceConversion), + _ => Err(format!("invalid value: {:?}", value)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::generic_inference_type::GenericInferenceType; + use crate::test_helpers::assert_serialization; + + #[test] + fn test_serialization() { + assert_serialization(GenericInferenceType::TextToSpeech, "text_to_speech"); + assert_serialization(GenericInferenceType::VoiceConversion, "voice_conversion"); + } + + #[test] + fn to_str() { + assert_eq!(GenericInferenceType::TextToSpeech.to_str(), "text_to_speech"); + assert_eq!(GenericInferenceType::VoiceConversion.to_str(), "voice_conversion"); + } + + #[test] + fn from_str() { + assert_eq!(GenericInferenceType::from_str("text_to_speech").unwrap(), GenericInferenceType::TextToSpeech); + assert_eq!(GenericInferenceType::from_str("voice_conversion").unwrap(), GenericInferenceType::VoiceConversion); + } +} diff --git a/crates/lib/reusable_types/src/lib.rs b/crates/lib/reusable_types/src/lib.rs index 6d0d911027..970ddfcc55 100644 --- a/crates/lib/reusable_types/src/lib.rs +++ b/crates/lib/reusable_types/src/lib.rs @@ -19,6 +19,7 @@ pub mod achievements; pub mod entity_visibility; pub mod generic_download_type; +pub mod generic_inference_type; pub mod server_environment; pub mod stripe; pub mod vocoder_type; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs index 9fe9c3f31e..4b68c0f6d3 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs @@ -10,7 +10,7 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; -use database_queries::queries::generic_download::insert_generic_download_job::{Args, insert_generic_download_job}; +use database_queries::queries::generic_download::web::insert_generic_download_job::{Args, insert_generic_download_job}; use database_queries::tokens::Tokens; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs index 6f870f78f2..cddda3a3c3 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs @@ -11,7 +11,7 @@ use chrono::{DateTime, Utc}; use crate::AnyhowResult; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use database_queries::queries::generic_download::get_generic_download_job_status::{GenericDownloadJobStatus, get_generic_download_job_status}; +use database_queries::queries::generic_download::web::get_generic_download_job_status::{GenericDownloadJobStatus, get_generic_download_job_status}; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; use r2d2_redis::redis::Commands; From 08c2736ea4ec637b03bcaeb88d8c367ce9e3f697 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 15 Dec 2022 02:10:18 -0500 Subject: [PATCH 04/54] More new tables: media_uploads, audit_logs --- .idea/dictionaries/bt.xml | 2 + .../2022-12-15-060826_media_uploads/down.sql | 5 + .../2022-12-15-060826_media_uploads/up.sql | 129 ++++++++++++++++++ .../2022-12-15-060830_audit_logs/down.sql | 5 + .../2022-12-15-060830_audit_logs/up.sql | 48 +++++++ 5 files changed, 189 insertions(+) create mode 100644 _migrations/2022-12-15-060826_media_uploads/down.sql create mode 100644 _migrations/2022-12-15-060826_media_uploads/up.sql create mode 100644 _migrations/2022-12-15-060830_audit_logs/down.sql create mode 100644 _migrations/2022-12-15-060830_audit_logs/up.sql diff --git a/.idea/dictionaries/bt.xml b/.idea/dictionaries/bt.xml index 50b698c5ca..d3699ac475 100644 --- a/.idea/dictionaries/bt.xml +++ b/.idea/dictionaries/bt.xml @@ -10,6 +10,8 @@ hifigan scrollback tacotron + transcoded + transcodings upserting vocoder vocoders diff --git a/_migrations/2022-12-15-060826_media_uploads/down.sql b/_migrations/2022-12-15-060826_media_uploads/down.sql new file mode 100644 index 0000000000..4f7f40fe13 --- /dev/null +++ b/_migrations/2022-12-15-060826_media_uploads/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +drop table media_uploads; diff --git a/_migrations/2022-12-15-060826_media_uploads/up.sql b/_migrations/2022-12-15-060826_media_uploads/up.sql new file mode 100644 index 0000000000..e78bd1f33e --- /dev/null +++ b/_migrations/2022-12-15-060826_media_uploads/up.sql @@ -0,0 +1,129 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE media_uploads ( + + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + -- This is so the in-progress results can be looked up by the UI. + token VARCHAR(32) NOT NULL, + + -- Idempotency token from client + -- This is so the frontend client doesn't submit duplicate jobs. + uuid_idempotency_token VARCHAR(36) NOT NULL, + + -- ========== MEDIA DETAILS ========== + + -- Type of media: + -- * 'audio' for wav, mp3, etc. + -- * 'video' for a variety of video types. + media_type VARCHAR(16) NOT NULL, + + -- The original filename of the media + maybe_original_filename VARCHAR(255) DEFAULT NULL, + + -- The original (non-transcoded or truncated) file size + original_file_size_bytes INT(10) NOT NULL DEFAULT 0, + + -- The original (non-transcoded or truncated) duration in millis. + original_duration_millis INT(10) NOT NULL DEFAULT 0, + + -- The original file's mime type. + maybe_original_mime_type VARCHAR(32) DEFAULT NULL, + + -- Audio encoder details + -- Only present if the file was audio or a video that had audio. + maybe_original_audio_encoding VARCHAR(32) DEFAULT NULL, + + -- Video encoder details + -- Only present if the file was a video. + maybe_original_video_encoding VARCHAR(32) DEFAULT NULL, + + -- For videos, the original frame width. + maybe_original_frame_width INT(5) DEFAULT NULL, + + -- For videos, the original frame height. + maybe_original_frame_height INT(5) DEFAULT NULL, + + -- Checksum of the original media + -- SHA1 hash [SHA2 = CHAR(64), SHA1 = CHAR(40), MD5 = CHAR(32)] + -- checksum_sha1 CHAR(40) NOT NULL, + + -- Checksum of the original media + -- SHA2 hash [SHA2 = CHAR(64), SHA1 = CHAR(40), MD5 = CHAR(32)] + -- Note that SHA2 is a hash family (SHA-228, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256, ...) + -- and produces different lengths output depending on the choice of algorithm. + checksum_sha2 CHAR(64) NOT NULL, + + -- ========== UPLOAD, TRANSCODING, AND TRUNCATION DETAILS ========== + + -- The directory this media is uploaded to will be exclusive for this file. + -- Only this given record will live in this bucket, but the directory may include + -- other transcodings or truncations. + public_bucket_directory_full_path VARCHAR(255) NOT NULL, + + -- We'll likely transcode (and potentially truncate) most media given to us. + -- This will store a json-encoded struct that details the changes. + -- TEXT = 65,535 bytes (64 KiB), ~= 4 bytes per UTF-8 character, ~ 16383 characters. + extra_file_modification_info TEXT DEFAULT NULL, + + -- ========== CREATOR DETAILS AND PREFERENCES ========== + + -- Foreign key to user + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + -- The creator can set a desired visibility for their data. + -- This does not always apply to every upload type. + -- Additionally, some upload types may require moderator approval prior + -- to being publicly listed, and this field has no bearing on that. + -- NB: DO NOT CHANGE THE ORDER OF THE ENUM VALUES DURING SCHEMA MIGRATIONS. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + -- The synthetic id associated with this result. + -- These ids are incremented on a per-user basis to help users + -- sequence their own work. They serve no other purpose. + maybe_creator_synthetic_id BIGINT(20) DEFAULT NULL, + + -- ========== MODERATION DETAILS ========== + + -- The last moderator that made changes last. + -- We should also build an audit table to supersede this. + maybe_mod_user_token VARCHAR(32) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- If this is removed by a mod or the creator. + user_deleted_at TIMESTAMP NULL, + mod_deleted_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + UNIQUE KEY (uuid_idempotency_token), + KEY index_media_type (media_type), + KEY index_checksum_sha2 (checksum_sha2), + KEY fk_maybe_creator_user_token (maybe_creator_user_token), + KEY fk_maybe_mod_user_token (maybe_mod_user_token), + KEY index_creator_ip_address (creator_ip_address), + KEY index_user_deleted_at (user_deleted_at), + KEY index_mod_deleted_at (mod_deleted_at) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations/2022-12-15-060830_audit_logs/down.sql b/_migrations/2022-12-15-060830_audit_logs/down.sql new file mode 100644 index 0000000000..10aae5c1a0 --- /dev/null +++ b/_migrations/2022-12-15-060830_audit_logs/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +drop table audit_logs; diff --git a/_migrations/2022-12-15-060830_audit_logs/up.sql b/_migrations/2022-12-15-060830_audit_logs/up.sql new file mode 100644 index 0000000000..bf902a8847 --- /dev/null +++ b/_migrations/2022-12-15-060830_audit_logs/up.sql @@ -0,0 +1,48 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE audit_logs ( + + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" of the audit log item (in case we add a UI or notes) + token VARCHAR(32) NOT NULL, + + -- The type of entity mutated + entity_type VARCHAR(32) NOT NULL, + + -- The token of the entity mutated + entity_token VARCHAR(32) NOT NULL, + + -- The type of action taken on the entity + entity_action VARCHAR(32) NOT NULL, + + -- Typically only signed in users will be performing actions on known entities, + -- but we're leaving this null just in case. This is the corresponding user that + -- made the change. + maybe_actor_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_actor_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- Whether the actor has some sort of moderator privilege. + is_actor_moderator BOOLEAN NOT NULL DEFAULT FALSE, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + actor_ip_address VARCHAR(40) NOT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY fk_entity_type (entity_type), + KEY fk_entity_token (entity_token), + KEY fk_maybe_actor_user_token (maybe_actor_user_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; From 46280c78777b42e6927d978a161be2c3d4719e17 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 15 Dec 2022 18:36:26 -0500 Subject: [PATCH 05/54] 3x voice conversion tables (models, results, synthetic_ids) --- .../down.sql | 5 + .../up.sql | 144 ++++++++++++++++++ .../down.sql | 5 + .../up.sql | 106 +++++++++++++ .../down.sql | 5 + .../up.sql | 25 +++ 6 files changed, 290 insertions(+) create mode 100644 _migrations/2022-12-15-223359_voice_conversion_models/down.sql create mode 100644 _migrations/2022-12-15-223359_voice_conversion_models/up.sql create mode 100644 _migrations/2022-12-15-223422_voice_conversion_results/down.sql create mode 100644 _migrations/2022-12-15-223422_voice_conversion_results/up.sql create mode 100644 _migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/down.sql create mode 100644 _migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/up.sql diff --git a/_migrations/2022-12-15-223359_voice_conversion_models/down.sql b/_migrations/2022-12-15-223359_voice_conversion_models/down.sql new file mode 100644 index 0000000000..211a6a85e0 --- /dev/null +++ b/_migrations/2022-12-15-223359_voice_conversion_models/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE voice_conversion_models; diff --git a/_migrations/2022-12-15-223359_voice_conversion_models/up.sql b/_migrations/2022-12-15-223359_voice_conversion_models/up.sql new file mode 100644 index 0000000000..5a4c84602d --- /dev/null +++ b/_migrations/2022-12-15-223359_voice_conversion_models/up.sql @@ -0,0 +1,144 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE voice_conversion_models ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + token VARCHAR(32) NOT NULL, + + -- ========== MODEL FUNCTIONALITY ========== + + -- The type of voice conversion model + -- eg. 'softvc', 'diffsvc' + model_type VARCHAR(32) NOT NULL, + + -- If set, we'll use the vocoder defined. + -- If not set, some global default will be used. + maybe_vocoder_token VARCHAR(32) DEFAULT NULL, + + -- ========== MODEL DESCRIPTION ========== + + -- The "name" of the voice model, which might be complicated. + -- If maybe_subject_token (etc.) is set, then it's authoritative instead. + title VARCHAR(255) NOT NULL, + + -- The description of the model in markdown. + description_markdown TEXT NOT NULL, + + -- Generated HTML (not user-editable). + description_rendered_html TEXT NOT NULL, + + -- The full IETF BCP47 language tag (eg. en, en-US, es-419, ja-JP, pt, etc.) + -- (Not that it matters apart from categorization, since voice conversion is universal.) + ietf_language_tag VARCHAR(64) NOT NULL DEFAULT 'en', + + -- The IETF BCP47 language tag's primary language subtag (eg. "es-419" becomes "es") + -- (Not that it matters apart from categorization, since voice conversion is universal.) + ietf_primary_language_subtag VARCHAR(12) NOT NULL DEFAULT 'en', + + -- ========== MODEL PLACEMENT ========== + + -- Whether the voice features on the front of FakeYou + is_front_page_featured BOOLEAN NOT NULL DEFAULT FALSE, + + -- ========== IMMUTABLE PROVENANCE AND METADATA ========== + + -- Where the file was originally downloaded (if it was downloaded) + original_download_url VARCHAR(512) DEFAULT NULL, + + -- The filename that was used at upload time (if available) + original_filename VARCHAR(255) DEFAULT NULL, + + -- File characteristics + file_size_bytes INT(10) NOT NULL DEFAULT 0, + + -- ========== CREATOR DETAILS ========== + + -- The person that created the template. + creator_user_token VARCHAR(32) NOT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6. + -- This will pick up moderator IP addresses, but a future audit logs table + -- will capture all edits. + ip_address_creation VARCHAR(40) NOT NULL, + ip_address_last_update VARCHAR(40) NOT NULL, + + -- The last user to edit the model. This could be the creator or a moderator. + -- Check the future audit logs table for more info. + maybe_last_update_user_token VARCHAR(32) DEFAULT NULL, + + -- (THIS MIGHT NOT BE USED) + -- NB: DO NOT SORT! + -- THIS MUST MATCH THE RESPECTIVE JOBS TABLE. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + -- ========== BUCKET STORAGE ========== + + -- The pytorch model + -- For now, this will be a hash of the file contents. + -- NB: NOT UNIQUE! We can allow duplicate uploads. + private_bucket_hash CHAR(64) NOT NULL, + + -- The "full url" version of the path + private_bucket_object_name VARCHAR(255) NOT NULL, + + -- ========== RATING AND RANKING ========== + + -- Calculated average, on a scale of 0-100 + -- Null with zero ratings. + -- calculated_average_score INT(3) DEFAULT NULL, + + -- Other metrics + -- calculated_total_ratings_submitted_count INT(10) NOT NULL DEFAULT 0, + -- calculated_total_uses_count BIGINT(10) NOT NULL DEFAULT 0, + + -- ========== MODERATION DETAILS ========== + + -- Mods may have to approve of vc models for them to show up in a public index. + -- Models can be used by the author (or other parties that know the URL) before + -- they're approved, but unapproved models won't show up in public indices. + -- This is independent of other visibility controls (is_mod_disabled, deleted_at, + -- and creator_set_visibility) + is_public_listing_approved BOOLEAN DEFAULT NULL, + + -- Extremely popular models may be locked from deletion or modification by users. + is_locked_from_user_modification BOOLEAN NOT NULL DEFAULT FALSE, + + -- If a moderator has comments. + maybe_mod_comments VARCHAR(255) DEFAULT NULL, + + -- ========== VECTOR CLOCK ========== + + -- Incremented with every update. + version INT NOT NULL DEFAULT 0, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- If this is removed by a mod. + -- It completely disappears from the system. + user_deleted_at TIMESTAMP NULL, + mod_deleted_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY index_model_type (model_type), + KEY fk_maybe_vocoder_token (maybe_vocoder_token), + KEY fk_creator_user_token (creator_user_token), + KEY index_creator_set_visibility (creator_set_visibility), + KEY index_private_bucket_hash (private_bucket_hash), + KEY index_is_public_listing_approved (is_public_listing_approved), + KEY index_is_locked_from_user_modification (is_locked_from_user_modification) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations/2022-12-15-223422_voice_conversion_results/down.sql b/_migrations/2022-12-15-223422_voice_conversion_results/down.sql new file mode 100644 index 0000000000..4ecacb5a65 --- /dev/null +++ b/_migrations/2022-12-15-223422_voice_conversion_results/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE voice_conversion_results; diff --git a/_migrations/2022-12-15-223422_voice_conversion_results/up.sql b/_migrations/2022-12-15-223422_voice_conversion_results/up.sql new file mode 100644 index 0000000000..d778adf413 --- /dev/null +++ b/_migrations/2022-12-15-223422_voice_conversion_results/up.sql @@ -0,0 +1,106 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE voice_conversion_results ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + token VARCHAR(32) NOT NULL, + + -- ========== INFERENCE DETAILS ========== + + -- The TTS model that was used + model_token VARCHAR(32) NOT NULL, + + -- If set, specifies which vocoder was used. + maybe_vocoder_token VARCHAR(64) DEFAULT NULL, + + -- Which media token was used (can be audio or video) + media_token VARCHAR(32) NOT NULL, + + -- ========== CREATOR DETAILS ========== + + -- The person that created the result + -- If the user wasn't logged in, this is null + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + -- (THIS MIGHT NOT BE USED) + -- NB: DO NOT SORT! + -- THIS MUST MATCH THE RESPECTIVE JOBS TABLE. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + -- The synthetic id associated with this result. + -- These ids are incremented on a per-user basis to help users + -- sequence their own work. They serve no other purpose. + maybe_creator_synthetic_id BIGINT(20) DEFAULT NULL, + + -- ========== BUCKET STORAGE ========== + + -- Full path where the wav, spectrogram, video, etc. are located. + public_bucket_directory_full_path VARCHAR(255) NOT NULL, + + -- Name of the wav audio file (if it exists) + public_bucket_wav_audio_object_name VARCHAR(255) NOT NULL, + + -- Name of the spectrogram (if it exists) + public_bucket_spectrogram_object_name VARCHAR(255) NOT NULL, + + -- Name of the video (if it exists) + public_bucket_video_object_name VARCHAR(255) NOT NULL, + + -- ========== METADATA ========== + + file_size_bytes INT(10) NOT NULL DEFAULT 0, + duration_millis INT(10) NOT NULL DEFAULT 0, + + -- ========== INFERENCE METADATA, DC, ROUTING, DEBUGGING ========== + + -- If true, the request was routed to a special "debug" worker. + is_debug_request BOOLEAN NOT NULL DEFAULT FALSE, + + is_generated_on_prem BOOLEAN NOT NULL DEFAULT FALSE, + generated_by_worker VARCHAR(255) DEFAULT NULL, + + -- ========== MODERATION DETAILS ========== + + -- The last user (possibily moderator) that made changes. + -- See the future audit logs table for better info and accounting. + maybe_last_update_user_token VARCHAR(32) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- If this is removed by a mod or the creator. + user_deleted_at TIMESTAMP NULL, + mod_deleted_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY fk_model_token (model_token), + KEY fk_maybe_vocoder_token (maybe_vocoder_token), + KEY fk_media_token (media_token), + KEY fk_maybe_creator_user_token (maybe_creator_user_token), + KEY fk_maybe_creator_anonymous_visitor_token (maybe_creator_anonymous_visitor_token), + KEY index_creator_ip_address (creator_ip_address), + KEY index_user_deleted_at(user_deleted_at), + KEY index_mod_deleted_at(mod_deleted_at), + KEY index_created_at(created_at) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/down.sql b/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/down.sql new file mode 100644 index 0000000000..ea17951df9 --- /dev/null +++ b/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE voice_conversion_result_synthetic_ids; diff --git a/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/up.sql b/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/up.sql new file mode 100644 index 0000000000..a950643840 --- /dev/null +++ b/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/up.sql @@ -0,0 +1,25 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- It gets unwieldy to show results without a numeric sequence. +-- We create a synthetic sequence for each user to make revisiting results easier. +CREATE TABLE voice_conversion_result_synthetic_ids ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + user_token VARCHAR(32) NOT NULL, + + -- The next result id. + -- After we use an id, we increment this counter. + next_id BIGINT(20) NOT NULL DEFAULT 0, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (user_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; From 60af0ea36dd40c7ef9b71d9ce034a5d58c01a801 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 16 Dec 2022 01:46:19 -0500 Subject: [PATCH 06/54] enum refactor, whew --- .../job/list_available_generic_download_jobs.rs | 8 ++++---- .../web/get_generic_download_job_status.rs | 4 ++-- .../web/insert_generic_download_job.rs | 4 ++-- .../job/list_available_generic_inference_jobs.rs | 8 ++++---- .../web/get_generic_inference_job_status.rs | 2 -- .../web/insert_generic_inference_job.rs | 4 ++-- .../tts/tts_models/get_tts_model_for_inference.rs | 4 ++-- .../tts_models/insert_tts_model_from_download_job.rs | 2 +- .../user_profiles/get_user_profile_by_username.rs | 6 +++--- .../src/queries/vocoder/get_vocoder_model.rs | 12 ++++++------ .../src/queries/vocoder/insert_vocoder_model.rs | 4 ++-- .../src/queries/vocoder/list_vocoder_models.rs | 10 +++++----- .../src/{ => db/enums}/entity_visibility.rs | 2 +- .../src/{ => db/enums}/generic_download_type.rs | 2 +- .../src/{ => db/enums}/generic_inference_type.rs | 2 +- crates/lib/reusable_types/src/db/enums/mod.rs | 7 +++++++ .../src/{ => db/enums}/vocoder_type.rs | 2 +- crates/lib/reusable_types/src/db/mod.rs | 2 ++ crates/lib/reusable_types/src/db/payloads/mod.rs | 0 crates/lib/reusable_types/src/lib.rs | 5 +---- .../users/src/endpoints/get_profile_handler.rs | 2 +- .../download_job/src/job_steps/process_single_job.rs | 2 +- .../src/job_types/hifigan/process_hifigan_vocoder.rs | 2 +- .../src/job_types/tacotron/process_tacotron_model.rs | 2 +- .../download_job/enqueue_generic_download.rs | 4 ++-- .../http_server/endpoints/vocoders/get_vocoder.rs | 4 ++-- .../http_server/endpoints/vocoders/list_vocoders.rs | 2 +- 27 files changed, 56 insertions(+), 52 deletions(-) rename crates/lib/reusable_types/src/{ => db/enums}/entity_visibility.rs (97%) rename crates/lib/reusable_types/src/{ => db/enums}/generic_download_type.rs (96%) rename crates/lib/reusable_types/src/{ => db/enums}/generic_inference_type.rs (96%) create mode 100644 crates/lib/reusable_types/src/db/enums/mod.rs rename crates/lib/reusable_types/src/{ => db/enums}/vocoder_type.rs (93%) create mode 100644 crates/lib/reusable_types/src/db/mod.rs create mode 100644 crates/lib/reusable_types/src/db/payloads/mod.rs diff --git a/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs b/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs index 973c6db434..9eeb944c39 100644 --- a/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs +++ b/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs @@ -4,8 +4,8 @@ use container_common::anyhow_result::AnyhowResult; use crate::column_types::job_status::JobStatus; use crate::queries::generic_download::job::_keys::GenericDownloadJobId; use crate::tokens::Tokens; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_download_type::GenericDownloadType; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::generic_download_type::GenericDownloadType; use sqlx::MySqlPool; use std::path::Path; use tokens::jobs::download::DownloadJobToken; @@ -45,9 +45,9 @@ SELECT creator_user_token, creator_ip_address, - creator_set_visibility as `creator_set_visibility: reusable_types::entity_visibility::EntityVisibility`, + creator_set_visibility as `creator_set_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, - download_type as `download_type: reusable_types::generic_download_type::GenericDownloadType`, + download_type as `download_type: reusable_types::db::enums::generic_download_type::GenericDownloadType`, download_url, title, diff --git a/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs b/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs index 0d0b4b5530..30e31960ec 100644 --- a/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs +++ b/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs @@ -2,8 +2,8 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use log::warn; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_download_type::GenericDownloadType; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::generic_download_type::GenericDownloadType; use sqlx::MySqlPool; use tokens::jobs::download::DownloadJobToken; diff --git a/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs b/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs index 8a0cc1057a..08d485dc1a 100644 --- a/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs +++ b/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_download_type::GenericDownloadType; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::generic_download_type::GenericDownloadType; use sqlx::MySqlPool; use tokens::jobs::download::DownloadJobToken; diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs b/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs index fdd5e2bebc..57aa3a3353 100644 --- a/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs +++ b/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs @@ -4,8 +4,8 @@ use container_common::anyhow_result::AnyhowResult; use crate::column_types::job_status::JobStatus; use crate::helpers::boolean_converters::i8_to_bool; use crate::queries::generic_inference::job::_keys::GenericInferenceJobId; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_inference_type::GenericInferenceType; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::generic_inference_type::GenericInferenceType; use sqlx::MySqlPool; use std::path::Path; use tokens::jobs::inference::InferenceJobToken; @@ -51,14 +51,14 @@ SELECT id as `id: crate::queries::generic_inference::job::_keys::GenericInferenceJobId`, token AS `inference_job_token: tokens::jobs::inference::InferenceJobToken`, - inference_type as `inference_type: reusable_types::generic_inference_type::GenericInferenceType`, + inference_type as `inference_type: reusable_types::db::enums::generic_inference_type::GenericInferenceType`, maybe_inference_args, maybe_raw_inference_text, maybe_model_token, maybe_creator_user_token, creator_ip_address, - creator_set_visibility as `creator_set_visibility: reusable_types::entity_visibility::EntityVisibility`, + creator_set_visibility as `creator_set_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, status as `status: crate::column_types::job_status::JobStatus`, diff --git a/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs b/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs index 7e16eb03ef..f9cffd230c 100644 --- a/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs +++ b/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs @@ -2,8 +2,6 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use log::warn; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_inference_type::GenericInferenceType; use sqlx::MySqlPool; use tokens::jobs::inference::InferenceJobToken; diff --git a/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs b/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs index fceee79e7a..94da5b75d2 100644 --- a/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs +++ b/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_inference_type::GenericInferenceType; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::generic_inference_type::GenericInferenceType; use sqlx::MySqlPool; use tokens::jobs::inference::InferenceJobToken; diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model_for_inference.rs b/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model_for_inference.rs index f3e840e224..330b3a4ca2 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model_for_inference.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model_for_inference.rs @@ -7,7 +7,7 @@ use chrono::{Utc, DateTime}; use log::warn; use sqlx::MySqlPool; use sqlx; -use reusable_types::vocoder_type::VocoderType; +use reusable_types::db::enums::vocoder_type::VocoderType; // TODO: Can probably just reuse another query. @@ -91,7 +91,7 @@ SELECT tts.maybe_default_pretrained_vocoder, tts.maybe_custom_vocoder_token, - vocoder.vocoder_type as `maybe_custom_vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `maybe_custom_vocoder_type: reusable_types::db::enums::vocoder_type::VocoderType`, vocoder.title as maybe_custom_vocoder_title, vocoder.private_bucket_hash as maybe_custom_vocoder_private_bucket_hash, diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs b/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs index 120a589edc..a65d0d756f 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs @@ -3,7 +3,7 @@ use chrono::Utc; use container_common::anyhow_result::AnyhowResult; use crate::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use crate::tokens::Tokens; -use reusable_types::entity_visibility::EntityVisibility; +use reusable_types::db::enums::entity_visibility::EntityVisibility; use sqlx::MySqlPool; use std::path::Path; diff --git a/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs b/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs index 310cdb4345..9d60218cd7 100644 --- a/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs +++ b/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use crate::helpers::boolean_converters::i8_to_bool; use log::{info, warn, log}; -use reusable_types::entity_visibility::EntityVisibility; +use reusable_types::db::enums::entity_visibility::EntityVisibility; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; @@ -109,8 +109,8 @@ SELECT profile_rendered_html, user_role_slug, disable_gravatar, - preferred_tts_result_visibility as `preferred_tts_result_visibility: reusable_types::entity_visibility::EntityVisibility`, - preferred_w2l_result_visibility as `preferred_w2l_result_visibility: reusable_types::entity_visibility::EntityVisibility`, + preferred_tts_result_visibility as `preferred_tts_result_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, + preferred_w2l_result_visibility as `preferred_w2l_result_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, discord_username, twitch_username, twitter_username, diff --git a/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs b/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs index 9ad4683a8b..b5244801b5 100644 --- a/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs +++ b/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs @@ -8,8 +8,8 @@ use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use crate::helpers::boolean_converters::i8_to_bool; use log::warn; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::vocoder_type::VocoderType; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::vocoder_type::VocoderType; use sqlx::MySqlPool; // FIXME: This is the old style of query scoping and shouldn't be copied. @@ -120,7 +120,7 @@ async fn select_including_deleted( r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: reusable_types::db::enums::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, @@ -137,7 +137,7 @@ SELECT vocoder.is_mod_disabled_from_author_use, vocoder.is_mod_author_editing_locked, - vocoder.creator_set_visibility as `creator_set_visibility: reusable_types::entity_visibility::EntityVisibility`, + vocoder.creator_set_visibility as `creator_set_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, vocoder.created_at, vocoder.updated_at, @@ -168,7 +168,7 @@ async fn select_without_deleted( r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: reusable_types::db::enums::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, @@ -185,7 +185,7 @@ SELECT vocoder.is_mod_disabled_from_author_use, vocoder.is_mod_author_editing_locked, - vocoder.creator_set_visibility as `creator_set_visibility: reusable_types::entity_visibility::EntityVisibility`, + vocoder.creator_set_visibility as `creator_set_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, vocoder.created_at, vocoder.updated_at, diff --git a/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs b/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs index f6152981a5..41b97722b2 100644 --- a/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs +++ b/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs @@ -6,8 +6,8 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; use crate::tokens::Tokens; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::vocoder_type::VocoderType; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::vocoder_type::VocoderType; use sqlx::MySqlPool; use std::path::Path; diff --git a/crates/lib/database_queries/src/queries/vocoder/list_vocoder_models.rs b/crates/lib/database_queries/src/queries/vocoder/list_vocoder_models.rs index 728eab0b95..e0d905866b 100644 --- a/crates/lib/database_queries/src/queries/vocoder/list_vocoder_models.rs +++ b/crates/lib/database_queries/src/queries/vocoder/list_vocoder_models.rs @@ -8,7 +8,7 @@ use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use crate::helpers::boolean_converters::i8_to_bool; use log::{warn, info}; -use reusable_types::vocoder_type::VocoderType; +use reusable_types::db::enums::vocoder_type::VocoderType; use sqlx::MySqlPool; // FIXME: This is the old style of query scoping and shouldn't be copied. @@ -115,7 +115,7 @@ async fn list_vocoder_models_for_all_creators( r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: reusable_types::db::enums::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, users.display_name as creator_display_name, @@ -147,7 +147,7 @@ WHERE r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: reusable_types::db::enums::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, users.display_name as creator_display_name, @@ -187,7 +187,7 @@ async fn list_vocoder_models_creator_scoped( r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: reusable_types::db::enums::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, users.display_name as creator_display_name, @@ -221,7 +221,7 @@ WHERE r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: reusable_types::db::enums::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, users.display_name as creator_display_name, diff --git a/crates/lib/reusable_types/src/entity_visibility.rs b/crates/lib/reusable_types/src/db/enums/entity_visibility.rs similarity index 97% rename from crates/lib/reusable_types/src/entity_visibility.rs rename to crates/lib/reusable_types/src/db/enums/entity_visibility.rs index 55fea828f7..5823899add 100644 --- a/crates/lib/reusable_types/src/entity_visibility.rs +++ b/crates/lib/reusable_types/src/db/enums/entity_visibility.rs @@ -39,7 +39,7 @@ impl EntityVisibility { #[cfg(test)] mod tests { - use crate::entity_visibility::EntityVisibility; + use crate::db::enums::entity_visibility::EntityVisibility; use crate::test_helpers::assert_serialization; #[test] diff --git a/crates/lib/reusable_types/src/generic_download_type.rs b/crates/lib/reusable_types/src/db/enums/generic_download_type.rs similarity index 96% rename from crates/lib/reusable_types/src/generic_download_type.rs rename to crates/lib/reusable_types/src/db/enums/generic_download_type.rs index 6a5d1fb289..e58420fb7c 100644 --- a/crates/lib/reusable_types/src/generic_download_type.rs +++ b/crates/lib/reusable_types/src/db/enums/generic_download_type.rs @@ -40,7 +40,7 @@ impl GenericDownloadType { #[cfg(test)] mod tests { - use crate::generic_download_type::GenericDownloadType; + use crate::db::enums::generic_download_type::GenericDownloadType; use crate::test_helpers::assert_serialization; #[test] diff --git a/crates/lib/reusable_types/src/generic_inference_type.rs b/crates/lib/reusable_types/src/db/enums/generic_inference_type.rs similarity index 96% rename from crates/lib/reusable_types/src/generic_inference_type.rs rename to crates/lib/reusable_types/src/db/enums/generic_inference_type.rs index 084fed383d..2bb31d1025 100644 --- a/crates/lib/reusable_types/src/generic_inference_type.rs +++ b/crates/lib/reusable_types/src/db/enums/generic_inference_type.rs @@ -34,7 +34,7 @@ impl GenericInferenceType { #[cfg(test)] mod tests { - use crate::generic_inference_type::GenericInferenceType; + use crate::db::enums::generic_inference_type::GenericInferenceType; use crate::test_helpers::assert_serialization; #[test] diff --git a/crates/lib/reusable_types/src/db/enums/mod.rs b/crates/lib/reusable_types/src/db/enums/mod.rs new file mode 100644 index 0000000000..1e65a74f6b --- /dev/null +++ b/crates/lib/reusable_types/src/db/enums/mod.rs @@ -0,0 +1,7 @@ +//! This is meant to handle `enum` or `enum`-like columns in the database. +//! These are not Rust-like enums (sum types) that can have internal payloads. + +pub mod entity_visibility; +pub mod generic_download_type; +pub mod generic_inference_type; +pub mod vocoder_type; diff --git a/crates/lib/reusable_types/src/vocoder_type.rs b/crates/lib/reusable_types/src/db/enums/vocoder_type.rs similarity index 93% rename from crates/lib/reusable_types/src/vocoder_type.rs rename to crates/lib/reusable_types/src/db/enums/vocoder_type.rs index 1581c99903..d262f48ad0 100644 --- a/crates/lib/reusable_types/src/vocoder_type.rs +++ b/crates/lib/reusable_types/src/db/enums/vocoder_type.rs @@ -23,8 +23,8 @@ impl VocoderType { #[cfg(test)] mod tests { + use crate::db::enums::vocoder_type::VocoderType; use crate::test_helpers::assert_serialization; - use crate::vocoder_type::VocoderType; #[test] fn test_serialization() { diff --git a/crates/lib/reusable_types/src/db/mod.rs b/crates/lib/reusable_types/src/db/mod.rs new file mode 100644 index 0000000000..9a873485f4 --- /dev/null +++ b/crates/lib/reusable_types/src/db/mod.rs @@ -0,0 +1,2 @@ +pub mod enums; +pub mod payloads; \ No newline at end of file diff --git a/crates/lib/reusable_types/src/db/payloads/mod.rs b/crates/lib/reusable_types/src/db/payloads/mod.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/lib/reusable_types/src/lib.rs b/crates/lib/reusable_types/src/lib.rs index 970ddfcc55..00be211168 100644 --- a/crates/lib/reusable_types/src/lib.rs +++ b/crates/lib/reusable_types/src/lib.rs @@ -17,9 +17,6 @@ #[cfg(test)] pub mod test_helpers; pub mod achievements; -pub mod entity_visibility; -pub mod generic_download_type; -pub mod generic_inference_type; +pub mod db; pub mod server_environment; pub mod stripe; -pub mod vocoder_type; diff --git a/crates/plugins/users/src/endpoints/get_profile_handler.rs b/crates/plugins/users/src/endpoints/get_profile_handler.rs index be46c3011c..e768701924 100644 --- a/crates/plugins/users/src/endpoints/get_profile_handler.rs +++ b/crates/plugins/users/src/endpoints/get_profile_handler.rs @@ -16,7 +16,7 @@ use http_server_common::request::get_request_header_optional::get_request_header use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use http_server_common::util::timer::MultiBenchmarkingTimer; use log::warn; -use reusable_types::entity_visibility::EntityVisibility; +use reusable_types::db::enums::entity_visibility::EntityVisibility; use sqlx::MySqlPool; use std::fmt; use tokens::users::user::UserToken; diff --git a/crates/service/job/download_job/src/job_steps/process_single_job.rs b/crates/service/job/download_job/src/job_steps/process_single_job.rs index 4c2a102337..cac9e81a53 100644 --- a/crates/service/job/download_job/src/job_steps/process_single_job.rs +++ b/crates/service/job/download_job/src/job_steps/process_single_job.rs @@ -10,7 +10,7 @@ use database_queries::queries::generic_download::job::mark_generic_download_job_ use database_queries::queries::generic_download::job::mark_generic_download_job_pending_and_grab_lock::mark_generic_download_job_pending_and_grab_lock; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; -use reusable_types::generic_download_type::GenericDownloadType; +use reusable_types::db::enums::generic_download_type::GenericDownloadType; use tempdir::TempDir; pub async fn process_single_job(job_state: &JobState, job: &AvailableDownloadJob) -> AnyhowResult<()> { diff --git a/crates/service/job/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs b/crates/service/job/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs index 12665dafbb..16c3cd11b5 100644 --- a/crates/service/job/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs +++ b/crates/service/job/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs @@ -10,7 +10,7 @@ use database_queries::queries::generic_download::job::list_available_generic_dow use database_queries::queries::vocoder::insert_vocoder_model::{Args, insert_vocoder_model}; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; -use reusable_types::vocoder_type::VocoderType; +use reusable_types::db::enums::vocoder_type::VocoderType; use std::fs::File; use std::io::Read; use std::path::PathBuf; diff --git a/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs b/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs index 95c2c9516e..5f9773a1ef 100644 --- a/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs +++ b/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs @@ -11,7 +11,7 @@ use database_queries::queries::tts::tts_models::insert_tts_model_from_download_j use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; -use reusable_types::entity_visibility::EntityVisibility; +use reusable_types::db::enums::entity_visibility::EntityVisibility; use std::fs::File; use std::io::Read; use std::path::PathBuf; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs index 4b68c0f6d3..eadf802447 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs @@ -16,8 +16,8 @@ use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; use regex::Regex; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_download_type::GenericDownloadType; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::generic_download_type::GenericDownloadType; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs index d71057def2..33cafb9655 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs @@ -12,8 +12,8 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use database_queries::queries::vocoder::get_vocoder_model::get_vocoder_model_by_token; use log::warn; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::vocoder_type::VocoderType; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::vocoder_type::VocoderType; use std::fmt; use std::sync::Arc; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs index 6cebb229c1..ce6cf287c9 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs @@ -11,7 +11,7 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use database_queries::queries::vocoder::list_vocoder_models::{list_vocoder_models, VocoderModelListItem}; use log::warn; -use reusable_types::vocoder_type::VocoderType; +use reusable_types::db::enums::vocoder_type::VocoderType; use std::fmt; use std::sync::Arc; From 91ac868369f861e6397abdbf7835dde234af0472 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 16 Dec 2022 01:56:05 -0500 Subject: [PATCH 07/54] new download types --- .../up.sql | 2 +- .../src/db/enums/generic_download_type.rs | 38 +++++++++++++++---- .../src/job_steps/process_single_job.rs | 5 +-- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/_migrations/2022-08-05-202017_generic_download_jobs/up.sql b/_migrations/2022-08-05-202017_generic_download_jobs/up.sql index 17a883e0a6..afcf500440 100644 --- a/_migrations/2022-08-05-202017_generic_download_jobs/up.sql +++ b/_migrations/2022-08-05-202017_generic_download_jobs/up.sql @@ -27,7 +27,7 @@ CREATE TABLE generic_download_jobs ( -- ========== UPLOAD DETAILS ========== -- A "well defined" type of upload - -- The first upload type will be "hifigan_model" + -- Examples: "hifigan", "tacotron2", etc. download_type VARCHAR(32) NOT NULL, -- Assume the same URL can be uploaded more than once (in case of errors or diff --git a/crates/lib/reusable_types/src/db/enums/generic_download_type.rs b/crates/lib/reusable_types/src/db/enums/generic_download_type.rs index e58420fb7c..2a7971cca7 100644 --- a/crates/lib/reusable_types/src/db/enums/generic_download_type.rs +++ b/crates/lib/reusable_types/src/db/enums/generic_download_type.rs @@ -1,18 +1,35 @@ /// Our "generic downloads" pipeline supports a wide variety of ML models and other media. +/// They are serialized in the database table `generic_download_jobs` as a VARCHAR(32). +/// /// Each type of download is identified by the following enum variants. /// These types are present in the HTTP API and database columns as serialized here. /// /// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. #[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] pub enum GenericDownloadType { + /// NB: Note - this is hifigan for Tacotron2. + /// Some work will be needed to unify this with other hifigan types. #[serde(rename = "hifigan")] #[sqlx(rename = "hifigan")] HifiGan, - #[serde(rename = "melgan_vocodes")] - #[sqlx(rename = "melgan_vocodes")] - MelGanVocodes, + /// NB: Note - this is hifigan for SoftVC (our internal codename is "rocketvc"). + /// Some work will need to be done to unify this with other hifigan types. + #[serde(rename = "hifigan_rocket_vc")] + #[sqlx(rename = "hifigan_rocket_vc")] + HifiGanRocketVc, + //#[serde(rename = "melgan_vocodes")] + //#[sqlx(rename = "melgan_vocodes")] + //MelGanVocodes, + + /// NB: Our external-facing name for "softvc" is rocketvc. + /// I wish we could stop being stupid about this. + #[serde(rename = "rocket_vc")] + #[sqlx(rename = "rocket_vc")] + RocketVc, + + /// Tacotron TTS models. #[serde(rename = "tacotron2")] #[sqlx(rename = "tacotron2")] Tacotron2, @@ -23,7 +40,8 @@ impl GenericDownloadType { pub fn to_str(&self) -> &'static str { match self { Self::HifiGan => "hifigan", - Self::MelGanVocodes => "melgan_vocodes", + Self::HifiGanRocketVc => "hifigan_rocket_vc", + Self::RocketVc => "rocket_vc", Self::Tacotron2 => "tacotron2", } } @@ -31,7 +49,8 @@ impl GenericDownloadType { pub fn from_str(value: &str) -> Result { match value { "hifigan" => Ok(Self::HifiGan), - "melgan_vocodes" => Ok(Self::MelGanVocodes), + "hifigan_rocket_vc" => Ok(Self::HifiGanRocketVc), + "rocket_vc" => Ok(Self::RocketVc), "tacotron2" => Ok(Self::Tacotron2), _ => Err(format!("invalid value: {:?}", value)), } @@ -46,21 +65,24 @@ mod tests { #[test] fn test_serialization() { assert_serialization(GenericDownloadType::HifiGan, "hifigan"); - assert_serialization(GenericDownloadType::MelGanVocodes, "melgan_vocodes"); + assert_serialization(GenericDownloadType::HifiGanRocketVc, "hifigan_rocket_vc"); + assert_serialization(GenericDownloadType::RocketVc, "rocket_vc"); assert_serialization(GenericDownloadType::Tacotron2, "tacotron2"); } #[test] fn to_str() { assert_eq!(GenericDownloadType::HifiGan.to_str(), "hifigan"); - assert_eq!(GenericDownloadType::MelGanVocodes.to_str(), "melgan_vocodes"); + assert_eq!(GenericDownloadType::HifiGanRocketVc.to_str(), "hifigan_rocket_vc"); + assert_eq!(GenericDownloadType::RocketVc.to_str(), "rocket_vc"); assert_eq!(GenericDownloadType::Tacotron2.to_str(), "tacotron2"); } #[test] fn from_str() { assert_eq!(GenericDownloadType::from_str("hifigan").unwrap(), GenericDownloadType::HifiGan); - assert_eq!(GenericDownloadType::from_str("melgan_vocodes").unwrap(), GenericDownloadType::MelGanVocodes); + assert_eq!(GenericDownloadType::from_str("hifigan_rocket_vc").unwrap(), GenericDownloadType::HifiGanRocketVc); + assert_eq!(GenericDownloadType::from_str("rocket_vc").unwrap(), GenericDownloadType::RocketVc); assert_eq!(GenericDownloadType::from_str("tacotron2").unwrap(), GenericDownloadType::Tacotron2); } } diff --git a/crates/service/job/download_job/src/job_steps/process_single_job.rs b/crates/service/job/download_job/src/job_steps/process_single_job.rs index cac9e81a53..a18ae24b1d 100644 --- a/crates/service/job/download_job/src/job_steps/process_single_job.rs +++ b/crates/service/job/download_job/src/job_steps/process_single_job.rs @@ -69,9 +69,6 @@ pub async fn process_single_job(job_state: &JobState, job: &AvailableDownloadJob entity_token = results.entity_token.clone(); entity_type = results.entity_type.clone(); } - GenericDownloadType::MelGanVocodes => { - return Err(anyhow!("MelGan not yet supported!")); - } GenericDownloadType::Tacotron2 => { let results = process_tacotron_model( job_state, @@ -83,6 +80,8 @@ pub async fn process_single_job(job_state: &JobState, job: &AvailableDownloadJob entity_token = results.entity_token.clone(); entity_type = results.entity_type.clone(); } + GenericDownloadType::HifiGanRocketVc => {} + GenericDownloadType::RocketVc => {} } // ===================================================== From 91695cde00a22c590cd99bbe8e44f149d5f95444 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 16 Dec 2022 02:41:40 -0500 Subject: [PATCH 08/54] downloader job: handle softvc hifigan --- .idea/dictionaries/bt.xml | 1 + .../up.sql | 1 + .../2022-08-05-202110_vocoder_models/up.sql | 4 +- .../src/mediators/badge_granter.rs | 86 ++++++++++- .../src/achievements/user_badge.rs | 60 +++++++- .../src/achievements/user_badge_type.rs | 3 + .../src/db/enums/vocoder_type.rs | 23 +++ .../src/bucket_path_unifier.rs | 1 + .../service/job/download_job/src/job_state.rs | 2 + .../src/job_steps/process_single_job.rs | 25 +++- .../hifigan/hifigan_model_check_command.rs | 2 +- .../hifigan_softvc_model_check_command.rs | 84 +++++++++++ .../src/job_types/hifigan_softvc/mod.rs | 2 + .../process_hifigan_softvc_vocoder.rs | 134 ++++++++++++++++++ .../job/download_job/src/job_types/mod.rs | 1 + 15 files changed, 421 insertions(+), 8 deletions(-) create mode 100644 crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs create mode 100644 crates/service/job/download_job/src/job_types/hifigan_softvc/mod.rs create mode 100644 crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs diff --git a/.idea/dictionaries/bt.xml b/.idea/dictionaries/bt.xml index d3699ac475..562630d84e 100644 --- a/.idea/dictionaries/bt.xml +++ b/.idea/dictionaries/bt.xml @@ -9,6 +9,7 @@ fakeyou hifigan scrollback + softvc tacotron transcoded transcodings diff --git a/_migrations/2022-08-05-202017_generic_download_jobs/up.sql b/_migrations/2022-08-05-202017_generic_download_jobs/up.sql index afcf500440..2b84c5bde7 100644 --- a/_migrations/2022-08-05-202017_generic_download_jobs/up.sql +++ b/_migrations/2022-08-05-202017_generic_download_jobs/up.sql @@ -22,6 +22,7 @@ CREATE TABLE generic_download_jobs ( -- The type of the object will vary based on the type of the upload, -- and we may include heuristics that auto-detect types in the future + -- Example types: 'hifigan', 'hifigan_rocket_vc' (codename for softvc), 'tacotron2', ... on_success_downloaded_entity_type VARCHAR(32) DEFAULT NULL, -- ========== UPLOAD DETAILS ========== diff --git a/_migrations/2022-08-05-202110_vocoder_models/up.sql b/_migrations/2022-08-05-202110_vocoder_models/up.sql index 1ee7c74c58..d11793c780 100644 --- a/_migrations/2022-08-05-202110_vocoder_models/up.sql +++ b/_migrations/2022-08-05-202110_vocoder_models/up.sql @@ -10,7 +10,9 @@ CREATE TABLE vocoder_models ( token VARCHAR(32) NOT NULL, -- The architecture of the vocoder. - -- Currently supported values: 'hifigan' + -- Currently supported values: + -- * 'hifigan' - hifigan for Tacotron2 + -- * 'hifigan_rocket_vc' - hifigan for SoftVC ("rocket" is a codename) vocoder_type VARCHAR(32) NOT NULL, -- ========== MUTABLE METADATA ========== diff --git a/crates/lib/database_queries/src/mediators/badge_granter.rs b/crates/lib/database_queries/src/mediators/badge_granter.rs index 34dcee3975..9adb4a85d2 100644 --- a/crates/lib/database_queries/src/mediators/badge_granter.rs +++ b/crates/lib/database_queries/src/mediators/badge_granter.rs @@ -38,7 +38,11 @@ impl BadgeGranter { let mut maybe_badge = None; - if count >= 250 { + if count >= 1000 { + maybe_badge = Some(UserBadge::TtsModelUploader1000); + } else if count >= 500 { + maybe_badge = Some(UserBadge::TtsModelUploader500); + } else if count >= 250 { maybe_badge = Some(UserBadge::TtsModelUploader250); } else if count >= 200 { maybe_badge = Some(UserBadge::TtsModelUploader200); @@ -133,7 +137,11 @@ impl BadgeGranter { let mut maybe_badge = None; - if count >= 250 { + if count >= 1000 { + maybe_badge = Some(UserBadge::VocoderModelUploader1000); + } else if count >= 500 { + maybe_badge = Some(UserBadge::VocoderModelUploader500); + } else if count >= 250 { maybe_badge = Some(UserBadge::VocoderModelUploader250); } else if count >= 200 { maybe_badge = Some(UserBadge::VocoderModelUploader200); @@ -173,6 +181,56 @@ impl BadgeGranter { Ok(()) } + /// This needs to be called *after* successful upload. + pub async fn maybe_grant_softvc_vocoder_model_uploads_badge(&self, user_token: &str) -> AnyhowResult<()> { + let count = self.count_softvc_vocoder_models_uploaded(user_token).await?; + + let mut maybe_badge = None; + + if count >= 1000 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader1000); + } else if count >= 500 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader500); + } else if count >= 250 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader250); + } else if count >= 200 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader200); + } else if count >= 150 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader150); + } else if count >= 100 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader100); + } else if count >= 50 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader50); + } else if count >= 20 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader20); + } else if count >= 10 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader10); + } else if count >= 5 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader5); + } else if count >= 1 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader1); + } + + let badge = match maybe_badge { + Some(badge) => badge, + None => return Ok(()), + }; + + if self.has_badge(user_token, badge).await? { + return Ok(()) + } + + let _record_id = self.insert( + badge, + user_token, + ).await?; + + self.firehose_publisher.publish_user_badge_granted(user_token, badge.to_db_value()) + .await?; + + Ok(()) + } + // ======================================================================= pub async fn has_badge(&self, user_token: &str, user_badge: UserBadge) -> AnyhowResult { @@ -245,6 +303,30 @@ SELECT count(*) as count FROM vocoder_models WHERE creator_user_token = ? + AND vocoder_type = 'hifigan' +LIMIT 1 + "#, + user_token + ) + .fetch_one(&self.mysql_pool) + .await; + + self.handle_count_query(maybe_result) + } + + async fn count_softvc_vocoder_models_uploaded( + &self, + user_token: &str, + ) -> AnyhowResult { + // NB: This could get expensive! + let maybe_result = sqlx::query_as!( + CountRecord, + r#" +SELECT count(*) as count +FROM vocoder_models +WHERE + creator_user_token = ? + AND vocoder_type = 'hifigan_rocket_vc' LIMIT 1 "#, user_token diff --git a/crates/lib/reusable_types/src/achievements/user_badge.rs b/crates/lib/reusable_types/src/achievements/user_badge.rs index e45f42732a..d204af398c 100644 --- a/crates/lib/reusable_types/src/achievements/user_badge.rs +++ b/crates/lib/reusable_types/src/achievements/user_badge.rs @@ -17,6 +17,8 @@ pub enum UserBadge { TtsModelUploader150, TtsModelUploader200, TtsModelUploader250, + TtsModelUploader500, + TtsModelUploader1000, // Granted for vocoder models VocoderModelUploader1, @@ -28,6 +30,21 @@ pub enum UserBadge { VocoderModelUploader150, VocoderModelUploader200, VocoderModelUploader250, + VocoderModelUploader500, + VocoderModelUploader1000, + + // Granted for vocoder (softvc aka "rocket_vc") models + VocoderRocketVcModelUploader1, + VocoderRocketVcModelUploader5, + VocoderRocketVcModelUploader10, + VocoderRocketVcModelUploader20, + VocoderRocketVcModelUploader50, + VocoderRocketVcModelUploader100, + VocoderRocketVcModelUploader150, + VocoderRocketVcModelUploader200, + VocoderRocketVcModelUploader250, + VocoderRocketVcModelUploader500, + VocoderRocketVcModelUploader1000, // Granted for uploading templates W2lTemplateUploader1, @@ -57,6 +74,7 @@ pub enum UserBadge { } impl UserBadge { + /// This is the value that lives in the database as a VARCHAR(32) pub fn to_db_value(&self) -> &'static str { match self { UserBadge::EarlyUser => "early_user", @@ -69,6 +87,9 @@ impl UserBadge { UserBadge::TtsModelUploader150 => "tts_model_uploader_150", UserBadge::TtsModelUploader200 => "tts_model_uploader_200", UserBadge::TtsModelUploader250 => "tts_model_uploader_250", + UserBadge::TtsModelUploader500 => "tts_model_uploader_500", + UserBadge::TtsModelUploader1000 => "tts_model_uploader_1000", + UserBadge::VocoderModelUploader1 => "vocoder_model_uploader_1", UserBadge::VocoderModelUploader5 => "vocoder_model_uploader_5", UserBadge::VocoderModelUploader10 => "vocoder_model_uploader_10", @@ -78,6 +99,21 @@ impl UserBadge { UserBadge::VocoderModelUploader150 => "vocoder_model_uploader_150", UserBadge::VocoderModelUploader200 => "vocoder_model_uploader_200", UserBadge::VocoderModelUploader250 => "vocoder_model_uploader_250", + UserBadge::VocoderModelUploader500 => "vocoder_model_uploader_500", + UserBadge::VocoderModelUploader1000 => "vocoder_model_uploader_1000", + + UserBadge::VocoderRocketVcModelUploader1 => "vocoder_rocket_vc_uploader_1", + UserBadge::VocoderRocketVcModelUploader5 => "vocoder_rocket_vc_uploader_5", + UserBadge::VocoderRocketVcModelUploader10 => "vocoder_rocket_vc_uploader_10", + UserBadge::VocoderRocketVcModelUploader20 => "vocoder_rocket_vc_uploader_20", + UserBadge::VocoderRocketVcModelUploader50 => "vocoder_rocket_vc_uploader_50", + UserBadge::VocoderRocketVcModelUploader100 => "vocoder_rocket_vc_uploader_100", + UserBadge::VocoderRocketVcModelUploader150 => "vocoder_rocket_vc_uploader_150", + UserBadge::VocoderRocketVcModelUploader200 => "vocoder_rocket_vc_uploader_200", + UserBadge::VocoderRocketVcModelUploader250 => "vocoder_rocket_vc_uploader_250", + UserBadge::VocoderRocketVcModelUploader500 => "vocoder_rocket_vc_uploader_500", + UserBadge::VocoderRocketVcModelUploader1000 => "vocoder_rocket_vc_uploader_1000", // NB: 31 characters, table max is 32 + UserBadge::W2lTemplateUploader1 => "w2l_template_uploader_1", UserBadge::W2lTemplateUploader10 => "w2l_template_uploader_10", UserBadge::W2lTemplateUploader50 => "w2l_template_uploader_50", @@ -88,6 +124,7 @@ impl UserBadge { UserBadge::W2lTemplateUploader2000 => "w2l_template_uploader_2000", UserBadge::W2lTemplateUploader5000 => "w2l_template_uploader_5000", UserBadge::W2lTemplateUploader10000 => "w2l_template_uploader_10000", + //UserBadge::TtsInferenceUser100 => "tts_inference_100", //UserBadge::TtsInferenceUser500 => "tts_inference_500", //UserBadge::TtsInferenceUser1000 => "tts_inference_1000", @@ -113,7 +150,10 @@ impl UserBadge { | UserBadge::TtsModelUploader100 | UserBadge::TtsModelUploader150 | UserBadge::TtsModelUploader200 - | UserBadge::TtsModelUploader250 => UserBadgeType::TtsModelUploader, + | UserBadge::TtsModelUploader250 + | UserBadge::TtsModelUploader500 + | UserBadge::TtsModelUploader1000 + => UserBadgeType::TtsModelUploader, UserBadge::VocoderModelUploader1 | UserBadge::VocoderModelUploader5 @@ -123,7 +163,23 @@ impl UserBadge { | UserBadge::VocoderModelUploader100 | UserBadge::VocoderModelUploader150 | UserBadge::VocoderModelUploader200 - | UserBadge::VocoderModelUploader250 => UserBadgeType::VocoderModelUploader, + | UserBadge::VocoderModelUploader250 + | UserBadge::VocoderModelUploader500 + | UserBadge::VocoderModelUploader1000 + => UserBadgeType::VocoderModelUploader, + + UserBadge::VocoderRocketVcModelUploader1 + | UserBadge::VocoderRocketVcModelUploader5 + | UserBadge::VocoderRocketVcModelUploader10 + | UserBadge::VocoderRocketVcModelUploader20 + | UserBadge::VocoderRocketVcModelUploader50 + | UserBadge::VocoderRocketVcModelUploader100 + | UserBadge::VocoderRocketVcModelUploader150 + | UserBadge::VocoderRocketVcModelUploader200 + | UserBadge::VocoderRocketVcModelUploader250 + | UserBadge::VocoderRocketVcModelUploader500 + | UserBadge::VocoderRocketVcModelUploader1000 + => UserBadgeType::VocoderRocketVcModelUploader, UserBadge::W2lTemplateUploader1 | UserBadge::W2lTemplateUploader10 diff --git a/crates/lib/reusable_types/src/achievements/user_badge_type.rs b/crates/lib/reusable_types/src/achievements/user_badge_type.rs index 41426374ad..f5c3be7ecf 100644 --- a/crates/lib/reusable_types/src/achievements/user_badge_type.rs +++ b/crates/lib/reusable_types/src/achievements/user_badge_type.rs @@ -11,6 +11,9 @@ pub enum UserBadgeType { /// Granted for uploading vocoder models VocoderModelUploader, + /// Granted for uploading vocoder (softvc aka "rocket_vc") models + VocoderRocketVcModelUploader, + /// Granted for uploading W2L templates W2lTemplateUploader, diff --git a/crates/lib/reusable_types/src/db/enums/vocoder_type.rs b/crates/lib/reusable_types/src/db/enums/vocoder_type.rs index d262f48ad0..f25e432b22 100644 --- a/crates/lib/reusable_types/src/db/enums/vocoder_type.rs +++ b/crates/lib/reusable_types/src/db/enums/vocoder_type.rs @@ -1,8 +1,16 @@ #[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] pub enum VocoderType { + /// NB: Note - this is hifigan for Tacotron2. + /// Some work will be needed to unify this with other hifigan types. #[serde(rename = "hifigan")] #[sqlx(rename = "hifigan")] HifiGan, + + /// NB: Note - this is hifigan for SoftVC (our internal codename is "rocketvc"). + /// Some work will need to be done to unify this with other hifigan types. + #[serde(rename = "hifigan_rocket_vc")] + #[sqlx(rename = "hifigan_rocket_vc")] + HifiGanRocketVc, } /// NB: Legacy API for older code. @@ -10,12 +18,14 @@ impl VocoderType { pub fn to_str(&self) -> &'static str { match self { Self::HifiGan=> "hifigan", + Self::HifiGanRocketVc => "hifigan_rocket_vc", } } pub fn from_str(value: &str) -> Result { match value { "hifigan" => Ok(Self::HifiGan), + "hifigan_rocket_vc" => Ok(Self::HifiGanRocketVc), _ => Err(format!("invalid value: {:?}", value)), } } @@ -29,5 +39,18 @@ mod tests { #[test] fn test_serialization() { assert_serialization(VocoderType::HifiGan, "hifigan"); + assert_serialization(VocoderType::HifiGanRocketVc, "hifigan_rocket_vc"); + } + + #[test] + fn to_str() { + assert_eq!(VocoderType::HifiGan.to_str(), "hifigan"); + assert_eq!(VocoderType::HifiGanRocketVc.to_str(), "hifigan_rocket_vc"); + } + + #[test] + fn from_str() { + assert_eq!(VocoderType::from_str("hifigan").unwrap(), VocoderType::HifiGan); + assert_eq!(VocoderType::from_str("hifigan_rocket_vc").unwrap(), VocoderType::HifiGanRocketVc); } } diff --git a/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs b/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs index a9611564d6..45dbc96c1d 100644 --- a/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs +++ b/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs @@ -66,6 +66,7 @@ impl BucketPathUnifier { } /// User-uploaded vocoders. + /// These can be HifiGan and HifiGanSoftVc pub fn vocoder_path(&self, vocoder_file_hash: &str) -> PathBuf { let hashed_path = Self::hashed_directory_path(vocoder_file_hash); let model_filename = format!("{}.pt", &vocoder_file_hash); diff --git a/crates/service/job/download_job/src/job_state.rs b/crates/service/job/download_job/src/job_state.rs index 18169265e3..82ded1e308 100644 --- a/crates/service/job/download_job/src/job_state.rs +++ b/crates/service/job/download_job/src/job_state.rs @@ -1,4 +1,5 @@ use crate::job_types::hifigan::hifigan_model_check_command::HifiGanModelCheckCommand; +use crate::job_types::hifigan_softvc::hifigan_softvc_model_check_command::HifiGanSoftVcModelCheckCommand; use crate::job_types::tacotron::tacotron_model_check_command::TacotronModelCheckCommand; use database_queries::mediators::badge_granter::BadgeGranter; use database_queries::mediators::firehose_publisher::FirehosePublisher; @@ -43,4 +44,5 @@ pub struct SidecarConfigs { pub google_drive_downloader: GoogleDriveDownloadCommand, pub tacotron_model_check_command: TacotronModelCheckCommand, pub hifigan_model_check_command: HifiGanModelCheckCommand, + pub hifigan_softvc_model_check_command: HifiGanSoftVcModelCheckCommand, } \ No newline at end of file diff --git a/crates/service/job/download_job/src/job_steps/process_single_job.rs b/crates/service/job/download_job/src/job_steps/process_single_job.rs index a18ae24b1d..b11d6daddd 100644 --- a/crates/service/job/download_job/src/job_steps/process_single_job.rs +++ b/crates/service/job/download_job/src/job_steps/process_single_job.rs @@ -4,6 +4,7 @@ use container_common::anyhow_result::AnyhowResult; use container_common::filesystem::safe_delete_temp_directory::safe_delete_temp_directory; use crate::JobState; use crate::job_types::hifigan::process_hifigan_vocoder::process_hifigan_vocoder; +use crate::job_types::hifigan_softvc::process_hifigan_softvc_vocoder::process_hifigan_softvc_vocoder; use crate::job_types::tacotron::process_tacotron_model::process_tacotron_model; use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use database_queries::queries::generic_download::job::mark_generic_download_job_done::mark_generic_download_job_done; @@ -69,6 +70,28 @@ pub async fn process_single_job(job_state: &JobState, job: &AvailableDownloadJob entity_token = results.entity_token.clone(); entity_type = results.entity_type.clone(); } + GenericDownloadType::HifiGanRocketVc => { + let results = process_hifigan_softvc_vocoder( + job_state, + job, + &temp_dir, + &download_filename, + &mut redis_logger, + ).await?; + entity_token = results.entity_token.clone(); + entity_type = results.entity_type.clone(); + } + GenericDownloadType::RocketVc => { + let results = process_softvc_model( + job_state, + job, + &temp_dir, + &download_filename, + &mut redis_logger, + ).await?; + entity_token = results.entity_token.clone(); + entity_type = results.entity_type.clone(); + } GenericDownloadType::Tacotron2 => { let results = process_tacotron_model( job_state, @@ -80,8 +103,6 @@ pub async fn process_single_job(job_state: &JobState, job: &AvailableDownloadJob entity_token = results.entity_token.clone(); entity_type = results.entity_type.clone(); } - GenericDownloadType::HifiGanRocketVc => {} - GenericDownloadType::RocketVc => {} } // ===================================================== diff --git a/crates/service/job/download_job/src/job_types/hifigan/hifigan_model_check_command.rs b/crates/service/job/download_job/src/job_types/hifigan/hifigan_model_check_command.rs index 37a8cc5d62..9bf645a400 100644 --- a/crates/service/job/download_job/src/job_types/hifigan/hifigan_model_check_command.rs +++ b/crates/service/job/download_job/src/job_types/hifigan/hifigan_model_check_command.rs @@ -1,7 +1,7 @@ use crate::AnyhowResult; use log::info; -use subprocess::{Popen, PopenConfig}; use std::path::Path; +use subprocess::{Popen, PopenConfig}; use subprocess_common::docker_options::DockerOptions; /// This command is used to check hifigan for being a real model diff --git a/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs b/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs new file mode 100644 index 0000000000..3cf34bb129 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs @@ -0,0 +1,84 @@ +use crate::AnyhowResult; +use log::info; +use std::path::Path; +use subprocess::{Popen, PopenConfig}; +use subprocess_common::docker_options::DockerOptions; + +/// This command is used to check hifigan for being a real model +#[derive(Clone)] +pub struct HifiGanSoftVcModelCheckCommand { + /// Where the HifiGan code lives + hifigan_softvc_root_code_directory: String, + + /// eg. `source python/bin/activate` + maybe_virtual_env_activation_command: Option, + + hifigan_softvc_model_check_script_name: String, + + /// If this is run under Docker (eg. in development), these are the options. + maybe_docker_options: Option, +} + +impl HifiGanSoftVcModelCheckCommand { + pub fn new( + hifigan_root_code_directory: &str, + maybe_virtual_env_activation_command: Option<&str>, + hifigan_model_check_script_name: &str, + maybe_docker_options: Option, + ) -> Self { + Self { + hifigan_softvc_root_code_directory: hifigan_root_code_directory.to_string(), + maybe_virtual_env_activation_command: maybe_virtual_env_activation_command.map(|s| s.to_string()), + hifigan_softvc_model_check_script_name: hifigan_model_check_script_name.to_string(), + maybe_docker_options, + } + } + + pub fn execute>( + &self, + checkpoint_path: P, + output_metadata_filename: P, + ) -> AnyhowResult<()> { + + let mut command = String::new(); + command.push_str(&format!("cd {}", self.hifigan_softvc_root_code_directory)); + + if let Some(venv_command) = self.maybe_virtual_env_activation_command.as_deref() { + command.push_str(" && "); + command.push_str(venv_command); + command.push_str(" "); + } + + command.push_str(" && "); + command.push_str("python "); + command.push_str(&self.hifigan_softvc_model_check_script_name); + command.push_str(" --checkpoint_path "); + command.push_str(&checkpoint_path.as_ref().display().to_string()); + command.push_str(" --output_metadata_filename "); + command.push_str(&output_metadata_filename.as_ref().display().to_string()); + + if let Some(docker_options) = self.maybe_docker_options.as_ref() { + command = docker_options.to_command_string(&command); + } + + info!("Command: {:?}", command); + + let command_parts = [ + "bash", + "-c", + &command + ]; + + let mut p = Popen::create(&command_parts, PopenConfig { + ..Default::default() + })?; + + info!("Pid : {:?}", p.pid()); + + let exit_status = p.wait()?; + + info!("Exit status: {:?}", exit_status); + + Ok(()) + } +} diff --git a/crates/service/job/download_job/src/job_types/hifigan_softvc/mod.rs b/crates/service/job/download_job/src/job_types/hifigan_softvc/mod.rs new file mode 100644 index 0000000000..144e48ea3a --- /dev/null +++ b/crates/service/job/download_job/src/job_types/hifigan_softvc/mod.rs @@ -0,0 +1,2 @@ +pub mod hifigan_softvc_model_check_command; +pub mod process_hifigan_softvc_vocoder; diff --git a/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs b/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs new file mode 100644 index 0000000000..a2af6ca215 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs @@ -0,0 +1,134 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use container_common::filesystem::check_file_exists::check_file_exists; +use container_common::filesystem::safe_delete_temp_directory::safe_delete_temp_directory; +use container_common::filesystem::safe_delete_temp_file::safe_delete_temp_file; +use container_common::hashing::hash_file_sha2::hash_file_sha2; +use crate::JobState; +use crate::job_steps::job_results::JobResults; +use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; +use database_queries::queries::vocoder::insert_vocoder_model::{Args, insert_vocoder_model}; +use jobs_common::redis_job_status_logger::RedisJobStatusLogger; +use log::{info, warn}; +use reusable_types::db::enums::vocoder_type::VocoderType; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use tempdir::TempDir; + +/// Returns the token of the entity. +pub async fn process_hifigan_softvc_vocoder<'a, 'b>( + job_state: &JobState, + job: &AvailableDownloadJob, + temp_dir: &TempDir, + download_filename: &str, + redis_logger: &'a mut RedisJobStatusLogger<'b>, +) -> AnyhowResult { + + // ==================== RUN MODEL CHECK ==================== // + + info!("Checking that model is valid..."); + + redis_logger.log_status("checking hifigan (softvc) model")?; + + let file_path = PathBuf::from(download_filename.clone()); + + let output_metadata_fs_path = temp_dir.path().join("metadata.json"); + + let model_check_result = job_state.sidecar_configs.hifigan_model_check_command.execute( + &file_path, + &output_metadata_fs_path + ); + + if let Err(e) = model_check_result { + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + return Err(anyhow!("model check error: {:?}", e)); + } + + // ==================== CHECK ALL FILES EXIST AND GET METADATA ==================== // + + info!("Checking that metadata output file exists..."); + + check_file_exists(&output_metadata_fs_path)?; + + let file_metadata = match read_metadata_file(&output_metadata_fs_path) { + Ok(metadata) => metadata, + Err(e) => { + safe_delete_temp_file(&file_path); + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_directory(&temp_dir); + return Err(e); + } + }; + + // ==================== UPLOAD MODEL FILE ==================== // + + info!("Uploading HifiGan (softvc) vocoder to GCS..."); + + let private_bucket_hash = hash_file_sha2(&download_filename)?; + + info!("File hash: {}", private_bucket_hash); + + let model_bucket_path = job_state.bucket_path_unifier.vocoder_path(&private_bucket_hash); + + info!("Destination bucket path: {:?}", &model_bucket_path); + + redis_logger.log_status("uploading hifigan model")?; + + if let Err(e) = job_state.bucket_client.upload_filename(&model_bucket_path, &file_path).await { + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + return Err(e); + } + + // ==================== DELETE DOWNLOADED FILE ==================== // + + // NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk... + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + + // ==================== SAVE RECORDS ==================== // + + info!("Saving model record..."); + let (_id, model_token) = insert_vocoder_model(Args { + vocoder_type: VocoderType::HifiGanRocketVc, // NB: "rocket_vc" is an internal codename for softvc. + title: &job.title, + original_download_url: &job.download_url, + original_filename: &download_filename, + file_size_bytes: file_metadata.file_size_bytes, + creator_user_token: &job.creator_user_token, + creator_ip_address: &job.creator_ip_address, + creator_set_visibility: job.creator_set_visibility, + private_bucket_hash: &private_bucket_hash, + private_bucket_object_name: &model_bucket_path, + mysql_pool: &job_state.mysql_pool + }).await?; + + job_state.badge_granter.maybe_grant_softvc_vocoder_model_uploads_badge(&job.creator_user_token) + .await + .map_err(|e| { + warn!("error maybe awarding badge: {:?}", e); + anyhow!("error maybe awarding badge") + })?; + + Ok(JobResults { + entity_token: Some(model_token), + entity_type: Some("hifigan_rocket_vc".to_string()), // NB: This may be different from `GenericDownloadType` in the future! + }) +} + +#[derive(Deserialize)] +struct FileMetadata { + pub file_size_bytes: u64, +} + +fn read_metadata_file(filename: &PathBuf) -> AnyhowResult { + let mut file = File::open(filename)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + Ok(serde_json::from_str(&buffer)?) +} + diff --git a/crates/service/job/download_job/src/job_types/mod.rs b/crates/service/job/download_job/src/job_types/mod.rs index 9f7e3cc1f8..c4ca220216 100644 --- a/crates/service/job/download_job/src/job_types/mod.rs +++ b/crates/service/job/download_job/src/job_types/mod.rs @@ -1,3 +1,4 @@ pub mod hifigan; +pub mod hifigan_softvc; pub mod tacotron; pub mod talknet; \ No newline at end of file From 13d00f6edff008faad4db77b08c5bf724c48e0f0 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 16 Dec 2022 03:07:20 -0500 Subject: [PATCH 09/54] partially handle softvc model downloads --- .../src/bucket_path_unifier.rs | 26 ++++ .../service/job/download_job/src/job_state.rs | 2 + .../hifigan_softvc_model_check_command.rs | 12 +- .../job/download_job/src/job_types/mod.rs | 1 + .../download_job/src/job_types/softvc/mod.rs | 2 + .../job_types/softvc/process_softvc_model.rs | 140 ++++++++++++++++++ .../softvc/softvc_model_check_command.rs | 87 +++++++++++ 7 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 crates/service/job/download_job/src/job_types/softvc/mod.rs create mode 100644 crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs create mode 100644 crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs diff --git a/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs b/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs index 45dbc96c1d..7ad5b8c9c3 100644 --- a/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs +++ b/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs @@ -16,6 +16,8 @@ pub struct BucketPathUnifier { pub w2l_inference_output_root: PathBuf, pub w2l_model_root: PathBuf, pub w2l_end_bump_root: PathBuf, + // VC + pub softvc_model_root: PathBuf, } impl BucketPathUnifier { @@ -33,6 +35,8 @@ impl BucketPathUnifier { w2l_inference_output_root: PathBuf::from("/w2l_inference_output"), w2l_model_root: PathBuf::from("/w2l_pretrained_models"), w2l_end_bump_root: PathBuf::from("/w2l_end_bumps"), + // VC + softvc_model_root: PathBuf::from("/user_uploaded_softvc_models"), } } @@ -169,6 +173,19 @@ impl BucketPathUnifier { .join(video_filename) } + // ==================== VOICE CONVERSION MODELS ==================== // + + pub fn softvc_model_path(&self, softvc_model_file_hash: &str) -> PathBuf { + let hashed_path = Self::hashed_directory_path(softvc_model_file_hash); + let model_filename = format!("{}.pt", &softvc_model_file_hash); + + self.softvc_model_root + .join(hashed_path) + .join(model_filename) + } + + // ==================== UTILITY ==================== // + pub fn hashed_directory_path(file_hash: &str) -> String { match file_hash.len() { 0 | 1=> "".to_string(), @@ -197,6 +214,8 @@ mod tests { w2l_inference_output_root: PathBuf::from("/test_path_w2l_output"), w2l_model_root: PathBuf::from("/test_path_w2l_pretrained_models"), w2l_end_bump_root: PathBuf::from("/test_path_w2l_end_bumps"), + // VOICE CONVERSION + softvc_model_root: PathBuf::from("/test_path_softvc_models"), } } @@ -297,6 +316,13 @@ mod tests { "/test_path_w2l_output/a/b/c/vocodes_video_TYPEABCDEF.mp4"); } + #[test] + fn test_softvc_model_path() { + let paths = get_instance(); + assert_eq!(paths.softvc_model_path("foobar").to_str().unwrap(), + "/test_path_softvc_models/f/o/o/foobar.pt"); + } + #[test] fn hashed_directory_path_length_zero() { assert_eq!(&BucketPathUnifier::hashed_directory_path(""), ""); diff --git a/crates/service/job/download_job/src/job_state.rs b/crates/service/job/download_job/src/job_state.rs index 82ded1e308..2ee2e40626 100644 --- a/crates/service/job/download_job/src/job_state.rs +++ b/crates/service/job/download_job/src/job_state.rs @@ -1,5 +1,6 @@ use crate::job_types::hifigan::hifigan_model_check_command::HifiGanModelCheckCommand; use crate::job_types::hifigan_softvc::hifigan_softvc_model_check_command::HifiGanSoftVcModelCheckCommand; +use crate::job_types::softvc::softvc_model_check_command::SoftVcModelCheckCommand; use crate::job_types::tacotron::tacotron_model_check_command::TacotronModelCheckCommand; use database_queries::mediators::badge_granter::BadgeGranter; use database_queries::mediators::firehose_publisher::FirehosePublisher; @@ -42,6 +43,7 @@ pub struct JobState { /// Configurations and interfaces to code deployed as sidecars or container mounts. pub struct SidecarConfigs { pub google_drive_downloader: GoogleDriveDownloadCommand, + pub softvc_model_check_command: SoftVcModelCheckCommand, pub tacotron_model_check_command: TacotronModelCheckCommand, pub hifigan_model_check_command: HifiGanModelCheckCommand, pub hifigan_softvc_model_check_command: HifiGanSoftVcModelCheckCommand, diff --git a/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs b/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs index 3cf34bb129..054b98f897 100644 --- a/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs +++ b/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs @@ -4,10 +4,10 @@ use std::path::Path; use subprocess::{Popen, PopenConfig}; use subprocess_common::docker_options::DockerOptions; -/// This command is used to check hifigan for being a real model +/// This command is used to check hifigan (softvc) for being a real model #[derive(Clone)] pub struct HifiGanSoftVcModelCheckCommand { - /// Where the HifiGan code lives + /// Where the HifiGan (softvc version) code lives hifigan_softvc_root_code_directory: String, /// eg. `source python/bin/activate` @@ -21,15 +21,15 @@ pub struct HifiGanSoftVcModelCheckCommand { impl HifiGanSoftVcModelCheckCommand { pub fn new( - hifigan_root_code_directory: &str, + hifigan_softvc_root_code_directory: &str, maybe_virtual_env_activation_command: Option<&str>, - hifigan_model_check_script_name: &str, + hifigan_softvc_model_check_script_name: &str, maybe_docker_options: Option, ) -> Self { Self { - hifigan_softvc_root_code_directory: hifigan_root_code_directory.to_string(), + hifigan_softvc_root_code_directory: hifigan_softvc_root_code_directory.to_string(), maybe_virtual_env_activation_command: maybe_virtual_env_activation_command.map(|s| s.to_string()), - hifigan_softvc_model_check_script_name: hifigan_model_check_script_name.to_string(), + hifigan_softvc_model_check_script_name: hifigan_softvc_model_check_script_name.to_string(), maybe_docker_options, } } diff --git a/crates/service/job/download_job/src/job_types/mod.rs b/crates/service/job/download_job/src/job_types/mod.rs index c4ca220216..8342328df3 100644 --- a/crates/service/job/download_job/src/job_types/mod.rs +++ b/crates/service/job/download_job/src/job_types/mod.rs @@ -1,4 +1,5 @@ pub mod hifigan; pub mod hifigan_softvc; +pub mod softvc; pub mod tacotron; pub mod talknet; \ No newline at end of file diff --git a/crates/service/job/download_job/src/job_types/softvc/mod.rs b/crates/service/job/download_job/src/job_types/softvc/mod.rs new file mode 100644 index 0000000000..87ade21b43 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/softvc/mod.rs @@ -0,0 +1,2 @@ +pub mod softvc_model_check_command; +pub mod process_softvc_model; diff --git a/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs new file mode 100644 index 0000000000..cc9d84f130 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs @@ -0,0 +1,140 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use container_common::filesystem::check_file_exists::check_file_exists; +use container_common::filesystem::safe_delete_temp_directory::safe_delete_temp_directory; +use container_common::filesystem::safe_delete_temp_file::safe_delete_temp_file; +use container_common::hashing::hash_file_sha2::hash_file_sha2; +use crate::JobState; +use crate::job_steps::job_results::JobResults; +use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; +use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job::insert_tts_model_from_download_job; +use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job; +use jobs_common::redis_job_status_logger::RedisJobStatusLogger; +use log::{info, warn}; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use tempdir::TempDir; + +/// Returns the token of the entity. +pub async fn process_softvc_model<'a, 'b>( + job_state: &JobState, + job: &AvailableDownloadJob, + temp_dir: &TempDir, + download_filename: &str, + redis_logger: &'a mut RedisJobStatusLogger<'b>, +) -> AnyhowResult { + + // ==================== RUN MODEL CHECK ==================== // + + info!("Checking that softvc model is valid..."); + + redis_logger.log_status("checking rocket vc model")?; // NB: "rocket vc" is a codename + + let file_path = PathBuf::from(download_filename.clone()); + + let output_metadata_fs_path = temp_dir.path().join("metadata.json"); + + let model_check_result = job_state.sidecar_configs.softvc_model_check_command.execute( + &file_path, + &output_metadata_fs_path, + ); + + if let Err(e) = model_check_result { + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + return Err(anyhow!("model check error: {:?}", e)); + } + + // ==================== CHECK ALL FILES EXIST AND GET METADATA ==================== // + + info!("Checking that metadata output file exists..."); + + check_file_exists(&output_metadata_fs_path)?; + + let file_metadata = match read_metadata_file(&output_metadata_fs_path) { + Ok(metadata) => metadata, + Err(e) => { + safe_delete_temp_file(&file_path); + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_directory(&temp_dir); + return Err(e); + } + }; + + // ==================== UPLOAD MODEL FILE ==================== // + + info!("Uploading SoftVC model to GCS..."); + + let private_bucket_hash = hash_file_sha2(&download_filename)?; + + info!("File hash: {}", private_bucket_hash); + + let model_bucket_path = job_state.bucket_path_unifier.softvc_model_path(&private_bucket_hash); + + info!("Destination bucket path: {:?}", &model_bucket_path); + + redis_logger.log_status("uploading rocket vc model")?; // NB: "rocket vc" is a codename + + if let Err(e) = job_state.bucket_client.upload_filename(&model_bucket_path, &file_path).await { + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + return Err(e); + } + + // ==================== DELETE DOWNLOADED FILE ==================== // + + // NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk... + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + + // ==================== SAVE RECORDS ==================== // + + info!("Saving Soft VC record..."); + + // TODO FIX BELOW QUERIES FOR SOFTVC -- + // TODO FIX BELOW QUERIES FOR SOFTVC -- + // TODO FIX BELOW QUERIES FOR SOFTVC -- + // TODO FIX BELOW QUERIES FOR SOFTVC -- + + let (_id, model_token) = insert_tts_model_from_download_job(insert_tts_model_from_download_job::Args { + title: &job.title, + original_download_url: &job.download_url, + original_filename: &download_filename, + file_size_bytes: file_metadata.file_size_bytes, + creator_user_token: &job.creator_user_token, + creator_ip_address: &job.creator_ip_address, + creator_set_visibility: EntityVisibility::Public, // TODO: All models default to public at start + private_bucket_hash: &private_bucket_hash, + private_bucket_object_name: &model_bucket_path, + mysql_pool: &job_state.mysql_pool, + }).await?; + + job_state.badge_granter.maybe_grant_tts_model_uploads_badge(&job.creator_user_token) + .await + .map_err(|e| { + warn!("error maybe awarding badge: {:?}", e); + anyhow!("error maybe awarding badge") + })?; + + Ok(JobResults { + entity_token: Some(model_token), + entity_type: Some("tacotron2".to_string()), // NB: This may be different from `GenericDownloadType` in the future! + }) +} + +#[derive(Deserialize)] +struct FileMetadata { + pub file_size_bytes: u64, +} + +fn read_metadata_file(filename: &PathBuf) -> AnyhowResult { + let mut file = File::open(filename)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + Ok(serde_json::from_str(&buffer)?) +} + diff --git a/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs b/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs new file mode 100644 index 0000000000..ba91ba5976 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs @@ -0,0 +1,87 @@ +use container_common::anyhow_result::AnyhowResult; +use log::info; +use std::fs::OpenOptions; +use std::path::Path; +use subprocess::{Popen, PopenConfig, Redirection}; +use subprocess_common::docker_options::DockerOptions; + +/// This command is used to check softvc for being a real model +#[derive(Clone)] +pub struct SoftVcModelCheckCommand { + /// Where the Tacotron code lives + softvc_root_code_directory: String, + + /// eg. `source python/bin/activate` + maybe_virtual_env_activation_command: Option, + + softvc_model_check_script_name: String, + + /// If this is run under Docker (eg. in development), these are the options. + maybe_docker_options: Option, +} + +impl SoftVcModelCheckCommand { + pub fn new( + softvc_root_code_directory: &str, + maybe_virtual_env_activation_command: Option<&str>, + softvc_model_check_script_name: &str, + maybe_docker_options: Option, + ) -> Self { + Self { + softvc_root_code_directory: softvc_root_code_directory.to_string(), + maybe_virtual_env_activation_command: maybe_virtual_env_activation_command.map(|s| s.to_string()), + softvc_model_check_script_name: softvc_model_check_script_name.to_string(), + maybe_docker_options, + } + } + + pub fn execute>( + &self, + synthesizer_checkpoint_path: P, + output_metadata_filename: P, + ) -> AnyhowResult<()> { + let mut command = String::new(); + + command.push_str("echo 'test'"); + command.push_str(" && "); + command.push_str(&format!("cd {}", self.softvc_root_code_directory)); + + if let Some(venv_command) = self.maybe_virtual_env_activation_command.as_deref() { + command.push_str(" && "); + command.push_str(venv_command); + command.push_str(" "); + } + + command.push_str(" && "); + command.push_str("python "); + command.push_str(&self.softvc_model_check_script_name); + command.push_str(" --synthesizer_checkpoint_path "); + command.push_str(&synthesizer_checkpoint_path.as_ref().display().to_string()); + command.push_str(" --output_metadata_filename "); + command.push_str(&output_metadata_filename.as_ref().display().to_string()); + + if let Some(docker_options) = self.maybe_docker_options.as_ref() { + command = docker_options.to_command_string(&command); + } + + info!("Command: {:?}", command); + + let command_parts = [ + "bash", + "-c", + &command + ]; + + let mut p = Popen::create(&command_parts, PopenConfig { + ..Default::default() + })?; + + info!("Pid : {:?}", p.pid()); + + let exit_status = p.wait()?; + + info!("Exit status: {:?}", exit_status); + + Ok(()) + } +} From 7bcde1e490bcb6c330b4cc06ac45d9f9ac4f038c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 16 Dec 2022 03:57:16 -0500 Subject: [PATCH 10/54] queries to handle softvc download (insert) --- .../up.sql | 2 +- .../lib/database_queries/src/queries/mod.rs | 1 + .../src/queries/voice_conversion/mod.rs | 1 + ...oice_conversion_model_from_download_job.rs | 75 +++++++++++++++++++ .../voice_conversion_models/mod.rs | 1 + crates/lib/tokens/src/lib.rs | 1 + crates/lib/tokens/src/prefixes.rs | 18 ++++- crates/lib/tokens/src/users/user.rs | 1 - crates/lib/tokens/src/voice_conversion/mod.rs | 1 + .../lib/tokens/src/voice_conversion/model.rs | 12 +++ .../src/job_steps/process_single_job.rs | 1 + .../job_types/softvc/process_softvc_model.rs | 16 ++-- 12 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 crates/lib/database_queries/src/queries/voice_conversion/mod.rs create mode 100644 crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs create mode 100644 crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/mod.rs create mode 100644 crates/lib/tokens/src/voice_conversion/mod.rs create mode 100644 crates/lib/tokens/src/voice_conversion/model.rs diff --git a/_migrations/2022-12-15-223359_voice_conversion_models/up.sql b/_migrations/2022-12-15-223359_voice_conversion_models/up.sql index 5a4c84602d..39d773ed28 100644 --- a/_migrations/2022-12-15-223359_voice_conversion_models/up.sql +++ b/_migrations/2022-12-15-223359_voice_conversion_models/up.sql @@ -12,7 +12,7 @@ CREATE TABLE voice_conversion_models ( -- ========== MODEL FUNCTIONALITY ========== -- The type of voice conversion model - -- eg. 'softvc', 'diffsvc' + -- eg. 'rocket_vc' (codename for 'softvc'), 'diffsvc' model_type VARCHAR(32) NOT NULL, -- If set, we'll use the vocoder defined. diff --git a/crates/lib/database_queries/src/queries/mod.rs b/crates/lib/database_queries/src/queries/mod.rs index 8f072bfb32..ca02151cc0 100644 --- a/crates/lib/database_queries/src/queries/mod.rs +++ b/crates/lib/database_queries/src/queries/mod.rs @@ -11,4 +11,5 @@ pub mod twitch; pub mod users; pub mod vocoder; pub mod voice_clone_requests; +pub mod voice_conversion; pub mod w2l; diff --git a/crates/lib/database_queries/src/queries/voice_conversion/mod.rs b/crates/lib/database_queries/src/queries/voice_conversion/mod.rs new file mode 100644 index 0000000000..3c7d77b643 --- /dev/null +++ b/crates/lib/database_queries/src/queries/voice_conversion/mod.rs @@ -0,0 +1 @@ +pub mod voice_conversion_models; \ No newline at end of file diff --git a/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs b/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs new file mode 100644 index 0000000000..c5dc7c23de --- /dev/null +++ b/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs @@ -0,0 +1,75 @@ +use anyhow::anyhow; +use chrono::Utc; +use container_common::anyhow_result::AnyhowResult; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use sqlx::MySqlPool; +use std::path::Path; +use tokens::voice_conversion::model::VoiceConversionModelToken; + +pub struct Args<'a, P: AsRef> { + pub title: &'a str, + + pub original_download_url: &'a str, + pub original_filename: &'a str, + pub file_size_bytes: u64, + + pub creator_user_token: &'a str, + pub creator_ip_address: &'a str, + pub creator_set_visibility: EntityVisibility, + + pub private_bucket_hash: &'a str, + pub private_bucket_object_name: P, + + pub mysql_pool: &'a MySqlPool, +} + + +pub async fn insert_voice_conversion_model_from_download_job>( + args: Args<'_, P>, +) -> AnyhowResult<(u64, VoiceConversionModelToken)> { + + let model_token = VoiceConversionModelToken::generate(); + + let private_bucket_object_name = &args.private_bucket_object_name + .as_ref() + .display() + .to_string(); + + // NB: 'rocket_vc' is codename for 'softvc' + let query_result = sqlx::query!( + r#" +INSERT INTO voice_conversion_models +SET + token = ?, + tts_model_type = "rocket_vc", + title = ?, + description_markdown = '', + description_rendered_html = '', + creator_user_token = ?, + creator_ip_address_creation = ?, + creator_ip_address_last_update = ?, + original_download_url = ?, + private_bucket_hash = ?, + private_bucket_object_name = ?, + file_size_bytes = ? + "#, + &model_token, + args.title, + args.creator_user_token, + args.creator_ip_address, + args.creator_ip_address, + args.original_download_url, + args.private_bucket_hash, + private_bucket_object_name, + args.file_size_bytes + ) + .execute(args.mysql_pool) + .await; + + let record_id = match query_result { + Ok(res) => res.last_insert_id(), + Err(err) => return Err(anyhow!("Mysql error: {:?}", err)), + }; + + Ok((record_id, model_token)) +} diff --git a/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/mod.rs b/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/mod.rs new file mode 100644 index 0000000000..63c6cdf401 --- /dev/null +++ b/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/mod.rs @@ -0,0 +1 @@ +pub mod insert_voice_conversion_model_from_download_job; diff --git a/crates/lib/tokens/src/lib.rs b/crates/lib/tokens/src/lib.rs index 2917558208..7de58f9d9e 100644 --- a/crates/lib/tokens/src/lib.rs +++ b/crates/lib/tokens/src/lib.rs @@ -39,3 +39,4 @@ pub (crate) mod prefixes; pub mod jobs; pub mod users; +pub mod voice_conversion; diff --git a/crates/lib/tokens/src/prefixes.rs b/crates/lib/tokens/src/prefixes.rs index 69b25d7dc0..4d7566fd43 100644 --- a/crates/lib/tokens/src/prefixes.rs +++ b/crates/lib/tokens/src/prefixes.rs @@ -16,15 +16,17 @@ pub(crate) enum EntityType { DownloadJob, InferenceJob, User, + VoiceConversionModel, } impl EntityType { pub fn prefix(self) -> &'static str { match self { - Self::DownloadJob => "jdown_:", // NB: Was "JGUP:" + Self::DownloadJob => "jdown_", // NB: Was "JGUP:" Self::InferenceJob => "jinf_", Self::User => "U:", // NB: Old-style prefix. + Self::VoiceConversionModel => "voco_", } } } @@ -59,4 +61,18 @@ mod tests { assert!(entities.len() > 0); assert_eq!(entities.len(), EntityType::COUNT); } + + #[test] + fn test_all_prefixes_end_with_separator() { + assert!(EntityType::iter() + .map(|entity| entity.prefix()) + .all(|prefix| prefix.ends_with(":") || prefix.ends_with("_"))); + } + + #[test] + fn test_all_prefixes_end_with_separator_length_one() { + for prefix in EntityType::iter().map(|entity| entity.prefix()) { + assert_eq!(prefix.len() - 1, prefix.replace(":", "").replace("_", "").len()); + } + } } diff --git a/crates/lib/tokens/src/users/user.rs b/crates/lib/tokens/src/users/user.rs index ed75f0b9bb..db1191e658 100644 --- a/crates/lib/tokens/src/users/user.rs +++ b/crates/lib/tokens/src/users/user.rs @@ -4,7 +4,6 @@ use serde::Serialize; use std::fmt::Debug; /// The primary key for users. -// https://docs.rs/sqlx/latest/sqlx/trait.Type.html #[derive(Clone, PartialEq, Eq, sqlx::Type, Debug, Serialize, Deserialize)] #[sqlx(transparent)] pub struct UserToken(pub String); diff --git a/crates/lib/tokens/src/voice_conversion/mod.rs b/crates/lib/tokens/src/voice_conversion/mod.rs new file mode 100644 index 0000000000..99bbd124c1 --- /dev/null +++ b/crates/lib/tokens/src/voice_conversion/mod.rs @@ -0,0 +1 @@ +pub mod model; \ No newline at end of file diff --git a/crates/lib/tokens/src/voice_conversion/model.rs b/crates/lib/tokens/src/voice_conversion/model.rs new file mode 100644 index 0000000000..6e38d1824c --- /dev/null +++ b/crates/lib/tokens/src/voice_conversion/model.rs @@ -0,0 +1,12 @@ +use crate::prefixes::EntityType; +use serde::Deserialize; +use serde::Serialize; +use std::fmt::Debug; + +/// The primary key for "generic" inference jobs. +#[derive(Clone, PartialEq, Eq, sqlx::Type, Debug, Serialize, Deserialize)] +#[sqlx(transparent)] +pub struct VoiceConversionModelToken(String); + +impl_string_token!(VoiceConversionModelToken); +impl_crockford_generator!(VoiceConversionModelToken, 16usize, EntityType::VoiceConversionModel, CrockfordLower); diff --git a/crates/service/job/download_job/src/job_steps/process_single_job.rs b/crates/service/job/download_job/src/job_steps/process_single_job.rs index b11d6daddd..42e9a75294 100644 --- a/crates/service/job/download_job/src/job_steps/process_single_job.rs +++ b/crates/service/job/download_job/src/job_steps/process_single_job.rs @@ -5,6 +5,7 @@ use container_common::filesystem::safe_delete_temp_directory::safe_delete_temp_d use crate::JobState; use crate::job_types::hifigan::process_hifigan_vocoder::process_hifigan_vocoder; use crate::job_types::hifigan_softvc::process_hifigan_softvc_vocoder::process_hifigan_softvc_vocoder; +use crate::job_types::softvc::process_softvc_model::process_softvc_model; use crate::job_types::tacotron::process_tacotron_model::process_tacotron_model; use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use database_queries::queries::generic_download::job::mark_generic_download_job_done::mark_generic_download_job_done; diff --git a/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs index cc9d84f130..65e37ad60b 100644 --- a/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs +++ b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs @@ -9,6 +9,7 @@ use crate::job_steps::job_results::JobResults; use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job::insert_tts_model_from_download_job; use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job; +use database_queries::queries::voice_conversion::voice_conversion_models::insert_voice_conversion_model_from_download_job::{Args, insert_voice_conversion_model_from_download_job}; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; use reusable_types::db::enums::entity_visibility::EntityVisibility; @@ -95,12 +96,7 @@ pub async fn process_softvc_model<'a, 'b>( info!("Saving Soft VC record..."); - // TODO FIX BELOW QUERIES FOR SOFTVC -- - // TODO FIX BELOW QUERIES FOR SOFTVC -- - // TODO FIX BELOW QUERIES FOR SOFTVC -- - // TODO FIX BELOW QUERIES FOR SOFTVC -- - - let (_id, model_token) = insert_tts_model_from_download_job(insert_tts_model_from_download_job::Args { + let (_id, model_token) = insert_voice_conversion_model_from_download_job(Args { title: &job.title, original_download_url: &job.download_url, original_filename: &download_filename, @@ -113,7 +109,7 @@ pub async fn process_softvc_model<'a, 'b>( mysql_pool: &job_state.mysql_pool, }).await?; - job_state.badge_granter.maybe_grant_tts_model_uploads_badge(&job.creator_user_token) + job_state.badge_granter.maybe_grant_softvc_vocoder_model_uploads_badge(&job.creator_user_token) .await .map_err(|e| { warn!("error maybe awarding badge: {:?}", e); @@ -121,8 +117,9 @@ pub async fn process_softvc_model<'a, 'b>( })?; Ok(JobResults { - entity_token: Some(model_token), - entity_type: Some("tacotron2".to_string()), // NB: This may be different from `GenericDownloadType` in the future! + entity_token: Some(model_token.to_string()), + // NB(1): "rocket_vc" is codename for softvc + entity_type: Some("rocket_vc".to_string()), // NB(2): This may be different from `GenericDownloadType` in the future! }) } @@ -137,4 +134,3 @@ fn read_metadata_file(filename: &PathBuf) -> AnyhowResult { file.read_to_string(&mut buffer)?; Ok(serde_json::from_str(&buffer)?) } - From 5f598dc516a9875148c353ccf86f923f650f5516 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 16 Dec 2022 04:24:19 -0500 Subject: [PATCH 11/54] Fix download-job queries and compile errors --- ...oice_conversion_model_from_download_job.rs | 6 +- .../job_types/softvc/process_softvc_model.rs | 2 - .../softvc/softvc_model_check_command.rs | 3 +- crates/service/job/download_job/src/main.rs | 62 +++++++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs b/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs index c5dc7c23de..9c70ad24be 100644 --- a/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs +++ b/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs @@ -41,13 +41,13 @@ pub async fn insert_voice_conversion_model_from_download_job>( INSERT INTO voice_conversion_models SET token = ?, - tts_model_type = "rocket_vc", + model_type = "rocket_vc", title = ?, description_markdown = '', description_rendered_html = '', creator_user_token = ?, - creator_ip_address_creation = ?, - creator_ip_address_last_update = ?, + ip_address_creation = ?, + ip_address_last_update = ?, original_download_url = ?, private_bucket_hash = ?, private_bucket_object_name = ?, diff --git a/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs index 65e37ad60b..1910929bae 100644 --- a/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs +++ b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs @@ -7,8 +7,6 @@ use container_common::hashing::hash_file_sha2::hash_file_sha2; use crate::JobState; use crate::job_steps::job_results::JobResults; use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; -use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job::insert_tts_model_from_download_job; -use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job; use database_queries::queries::voice_conversion::voice_conversion_models::insert_voice_conversion_model_from_download_job::{Args, insert_voice_conversion_model_from_download_job}; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; diff --git a/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs b/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs index ba91ba5976..18e0b9c717 100644 --- a/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs +++ b/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs @@ -1,8 +1,7 @@ use container_common::anyhow_result::AnyhowResult; use log::info; -use std::fs::OpenOptions; use std::path::Path; -use subprocess::{Popen, PopenConfig, Redirection}; +use subprocess::{Popen, PopenConfig}; use subprocess_common::docker_options::DockerOptions; /// This command is used to check softvc for being a real model diff --git a/crates/service/job/download_job/src/main.rs b/crates/service/job/download_job/src/main.rs index fe9117413d..6c63e0a41d 100644 --- a/crates/service/job/download_job/src/main.rs +++ b/crates/service/job/download_job/src/main.rs @@ -29,6 +29,8 @@ use container_common::filesystem::check_directory_exists::check_directory_exists use crate::job_state::{JobState, SidecarConfigs}; use crate::job_steps::main_loop::main_loop; use crate::job_types::hifigan::hifigan_model_check_command::HifiGanModelCheckCommand; +use crate::job_types::hifigan_softvc::hifigan_softvc_model_check_command::HifiGanSoftVcModelCheckCommand; +use crate::job_types::softvc::softvc_model_check_command::SoftVcModelCheckCommand; use crate::job_types::tacotron::tacotron_model_check_command::TacotronModelCheckCommand; use database_queries::mediators::badge_granter::BadgeGranter; use database_queries::mediators::firehose_publisher::FirehosePublisher; @@ -123,6 +125,35 @@ async fn main() -> AnyhowResult<()> { ) }; + let softvc_model_check_command = { + let root_directory = easyenv::get_env_string_required( + "SOFTVC_MODEL_CHECK_ROOT_DIRECTORY")?; + + let python_command = easyenv::get_env_string_or_default( + "SOFTVC_MODEL_CHECK_COMMAND", + "./vocodes_model_check_tacotron.py"); + + let maybe_venv_command = easyenv::get_env_string_optional( + "SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND"); + + let maybe_docker_options = easyenv::get_env_string_optional( + "SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE") + .map(|image_name| { + DockerOptions { + image_name, + maybe_bind_mount: Some(DockerFilesystemMount::tmp_to_tmp()), + maybe_gpu: Some(DockerGpu::All), + } + }); + + SoftVcModelCheckCommand::new( + &root_directory, + maybe_venv_command.as_deref(), + &python_command, + maybe_docker_options, + ) + }; + let tacotron_model_check_command = { let root_directory = easyenv::get_env_string_required( "TACOTRON_MODEL_CHECK_ROOT_DIRECTORY")?; @@ -181,6 +212,35 @@ async fn main() -> AnyhowResult<()> { ) }; + let hifigan_softvc_model_check_command= { + let root_directory = easyenv::get_env_string_required( + "HIFIGAN_SOFTVC_MODEL_CHECK_ROOT_DIRECTORY")?; + + let python_command = easyenv::get_env_string_or_default( + "HIFIGAN_SOFTVC_MODEL_CHECK_COMMAND", + "./vocodes_model_check_tacotron.py"); + + let maybe_venv_command = easyenv::get_env_string_optional( + "HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND"); + + let maybe_docker_options = easyenv::get_env_string_optional( + "HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE") + .map(|image_name| { + DockerOptions { + image_name, + maybe_bind_mount: Some(DockerFilesystemMount::tmp_to_tmp()), + maybe_gpu: Some(DockerGpu::All), + } + }); + + HifiGanSoftVcModelCheckCommand::new( + &root_directory, + maybe_venv_command.as_deref(), + &python_command, + maybe_docker_options, + ) + }; + // =============== End Configure Python "Sidecars" =============== let temp_directory = PathBuf::from(temp_directory); @@ -230,8 +290,10 @@ async fn main() -> AnyhowResult<()> { badge_granter, sidecar_configs: SidecarConfigs { google_drive_downloader, + softvc_model_check_command, tacotron_model_check_command, hifigan_model_check_command, + hifigan_softvc_model_check_command, }, job_batch_wait_millis: common_env.job_batch_wait_millis, job_max_attempts: common_env.job_max_attempts as i32, From 51350e6e5cace66dd211ee0b6fb704f19a0c63b6 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 16 Dec 2022 05:29:56 -0500 Subject: [PATCH 12/54] default parameters for local download job --- .env-download-job | 8 ++++++++ crates/service/job/download_job/src/main.rs | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.env-download-job b/.env-download-job index 032eac7e27..29cb87d8fe 100644 --- a/.env-download-job +++ b/.env-download-job @@ -6,11 +6,19 @@ WEB_DOWNLOADER_COMMAND=./download_internet_file.py WEB_DOWNLOADER_MAYBE_VENV_SCRIPT=./python/bin/activate WEB_DOWNLOADER_MAYBE_DOCKER_IMAGE=d73f28ce3ff6 +SOFTVC_MODEL_CHECK_ROOT_DIRECTORY=/ +SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" +SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=036b23daa586 + TACOTRON_MODEL_CHECK_ROOT_DIRECTORY=/models/tts TACOTRON_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" TACOTRON_MODEL_CHECK_COMMAND=./vocodes_model_check_tacotron.py TACOTRON_MODEL_CHECK_MAYBE_DOCKER_IMAGE=5642d0fd7fc1 +HIFIGAN_SOFTVC_MODEL_CHECK_ROOT_DIRECTORY=/ +HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" +HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=036b23daa586 + HIFIGAN_MODEL_CHECK_ROOT_DIRECTORY=/models/tts HIFIGAN_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" HIFIGAN_MODEL_CHECK_COMMAND=./vocodes_model_check_hifigan.py diff --git a/crates/service/job/download_job/src/main.rs b/crates/service/job/download_job/src/main.rs index 6c63e0a41d..05518dad7b 100644 --- a/crates/service/job/download_job/src/main.rs +++ b/crates/service/job/download_job/src/main.rs @@ -131,7 +131,7 @@ async fn main() -> AnyhowResult<()> { let python_command = easyenv::get_env_string_or_default( "SOFTVC_MODEL_CHECK_COMMAND", - "./vocodes_model_check_tacotron.py"); + "./model_check_softvc_acoustic.py"); let maybe_venv_command = easyenv::get_env_string_optional( "SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND"); @@ -218,7 +218,7 @@ async fn main() -> AnyhowResult<()> { let python_command = easyenv::get_env_string_or_default( "HIFIGAN_SOFTVC_MODEL_CHECK_COMMAND", - "./vocodes_model_check_tacotron.py"); + "./model_check_hifigan.py"); let maybe_venv_command = easyenv::get_env_string_optional( "HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND"); From c2f4c2c057f08705b7dc39d5e7855581aec72054 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 17 Dec 2022 12:25:05 -0500 Subject: [PATCH 13/54] Fix hifigan (softvc) model check --- .env-download-job | 4 ++-- .../job/download_job/src/job_steps/process_single_job.rs | 2 ++ .../hifigan_softvc/process_hifigan_softvc_vocoder.rs | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.env-download-job b/.env-download-job index 29cb87d8fe..a2d17913b0 100644 --- a/.env-download-job +++ b/.env-download-job @@ -8,7 +8,7 @@ WEB_DOWNLOADER_MAYBE_DOCKER_IMAGE=d73f28ce3ff6 SOFTVC_MODEL_CHECK_ROOT_DIRECTORY=/ SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" -SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=036b23daa586 +SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=a79106991ce3 TACOTRON_MODEL_CHECK_ROOT_DIRECTORY=/models/tts TACOTRON_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" @@ -17,7 +17,7 @@ TACOTRON_MODEL_CHECK_MAYBE_DOCKER_IMAGE=5642d0fd7fc1 HIFIGAN_SOFTVC_MODEL_CHECK_ROOT_DIRECTORY=/ HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" -HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=036b23daa586 +HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=a79106991ce3 HIFIGAN_MODEL_CHECK_ROOT_DIRECTORY=/models/tts HIFIGAN_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" diff --git a/crates/service/job/download_job/src/job_steps/process_single_job.rs b/crates/service/job/download_job/src/job_steps/process_single_job.rs index 42e9a75294..15ed5e3814 100644 --- a/crates/service/job/download_job/src/job_steps/process_single_job.rs +++ b/crates/service/job/download_job/src/job_steps/process_single_job.rs @@ -28,6 +28,8 @@ pub async fn process_single_job(job_state: &JobState, job: &AvailableDownloadJob return Ok(()) } + info!("Beginning work on {:?} = {} | {}", job.download_type, job.download_job_token, job.download_url); + // ==================== SETUP TEMP DIRS ==================== // let temp_dir = format!("temp_{}", job.id.0); diff --git a/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs b/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs index a2af6ca215..029f36b735 100644 --- a/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs +++ b/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs @@ -35,7 +35,7 @@ pub async fn process_hifigan_softvc_vocoder<'a, 'b>( let output_metadata_fs_path = temp_dir.path().join("metadata.json"); - let model_check_result = job_state.sidecar_configs.hifigan_model_check_command.execute( + let model_check_result = job_state.sidecar_configs.hifigan_softvc_model_check_command.execute( &file_path, &output_metadata_fs_path ); From 2fad96544638643a0df315e05ec15f228819d087 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 17 Dec 2022 12:30:53 -0500 Subject: [PATCH 14/54] "analytics" query to soft delete tts results for a given model --- analytics/tts_results.sql | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/analytics/tts_results.sql b/analytics/tts_results.sql index 7684820a6f..4b0db1a83d 100644 --- a/analytics/tts_results.sql +++ b/analytics/tts_results.sql @@ -31,3 +31,20 @@ from ( ) as sample where sample.is_generated_on_prem IS TRUE; + +-- Find TTS results for a single model +select count(*) from tts_results +where model_token = 'TM:ztt5s1be5tq6'; + +-- Find TTS results for a single model that are not deleted +select count(*) from tts_results +where model_token = 'TM:ztt5s1be5tq6' +and mod_deleted_at is NULL; + +-- Delete TTS results for a single model +-- Do this in short batches so a lock isn't held for prohibitively long. +update tts_results +set mod_deleted_at = NOW() +where model_token = 'TM:ztt5s1be5tq6' +and mod_deleted_at IS NULL +limit 5000; From 26587e51e3d7adcccdf8e760b0351cd40b29af88 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 17 Dec 2022 13:59:13 -0500 Subject: [PATCH 15/54] More "analytics" (more operational) queries for w2l jobs --- analytics/w2l_inference_jobs.sql | 7 +++++++ analytics/w2l_template_upload_jobs.sql | 28 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 analytics/w2l_inference_jobs.sql create mode 100644 analytics/w2l_template_upload_jobs.sql diff --git a/analytics/w2l_inference_jobs.sql b/analytics/w2l_inference_jobs.sql new file mode 100644 index 0000000000..40ccdb6cca --- /dev/null +++ b/analytics/w2l_inference_jobs.sql @@ -0,0 +1,7 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Pending job count +select count(*) from w2l_inference_jobs where status='pending'; + diff --git a/analytics/w2l_template_upload_jobs.sql b/analytics/w2l_template_upload_jobs.sql new file mode 100644 index 0000000000..5f7202347d --- /dev/null +++ b/analytics/w2l_template_upload_jobs.sql @@ -0,0 +1,28 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Get a count of pending jobs +select count(*) from w2l_template_upload_jobs where status='pending'; + +-- See pending download URLs: +select download_url from w2l_template_upload_jobs where status='pending'; + +-- See old jobs that might be stuck as "started" +select count(*) +from w2l_template_upload_jobs +where status='started' +and created_at < ( CURDATE() - INTERVAL 1 DAY ); + +-- Unstick old jobs that might be stuck as "started" +update w2l_template_upload_jobs +set status='dead' +where status='started' + and created_at < ( CURDATE() - INTERVAL 1 DAY ); + +-- Kill pending jobs with tiktok URLs: +-- These need to be blocked in the server and job directly. +update w2l_template_upload_jobs +set status='dead' +where status='pending' + and download_url LIKE '%tiktok%'; From 8591f74ff205e8c2ad1e04305fd2bed29b08d545 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 19 Dec 2022 17:41:59 -0500 Subject: [PATCH 16/54] Look at "started" w2l jobs too --- analytics/w2l_template_upload_jobs.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytics/w2l_template_upload_jobs.sql b/analytics/w2l_template_upload_jobs.sql index 5f7202347d..ceafb1a5e0 100644 --- a/analytics/w2l_template_upload_jobs.sql +++ b/analytics/w2l_template_upload_jobs.sql @@ -6,7 +6,7 @@ select count(*) from w2l_template_upload_jobs where status='pending'; -- See pending download URLs: -select download_url from w2l_template_upload_jobs where status='pending'; +select download_url from w2l_template_upload_jobs where status IN ('pending', 'started', 'attempt_failed'); -- See old jobs that might be stuck as "started" select count(*) From deb3671be6c71054d86a210d548164b1a9305909 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 20 Dec 2022 01:13:28 -0500 Subject: [PATCH 17/54] Endpoint for audio uploading --- .../2022-12-15-060826_media_uploads/up.sql | 2 + .../src/mediators/firehose_publisher.rs | 27 ++- .../media_uploads/insert_media_upload.rs | 100 ++++++++++ .../src/queries/media_uploads/mod.rs | 1 + .../lib/database_queries/src/queries/mod.rs | 1 + .../get_user_session_by_token.rs | 10 +- .../src/db/enums/media_upload_type.rs | 53 +++++ crates/lib/reusable_types/src/db/enums/mod.rs | 1 + .../src/db/payloads/media_upload_details.rs | 0 .../lib/reusable_types/src/db/payloads/mod.rs | 5 + crates/lib/tokens/src/files/media_upload.rs | 12 ++ crates/lib/tokens/src/files/mod.rs | 1 + crates/lib/tokens/src/lib.rs | 1 + crates/lib/tokens/src/prefixes.rs | 2 + .../media_uploads/drain_multipart_request.rs | 57 ++++++ .../endpoints/media_uploads/mod.rs | 3 + .../endpoints/media_uploads/upload_audio.rs | 187 ++++++++++++++++++ .../endpoints/media_uploads/upload_video.rs | 0 .../src/http_server/endpoints/mod.rs | 1 + .../service/web/storyteller_web/src/routes.rs | 27 ++- 20 files changed, 485 insertions(+), 6 deletions(-) create mode 100644 crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs create mode 100644 crates/lib/database_queries/src/queries/media_uploads/mod.rs create mode 100644 crates/lib/reusable_types/src/db/enums/media_upload_type.rs create mode 100644 crates/lib/reusable_types/src/db/payloads/media_upload_details.rs create mode 100644 crates/lib/tokens/src/files/media_upload.rs create mode 100644 crates/lib/tokens/src/files/mod.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/mod.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_video.rs diff --git a/_migrations/2022-12-15-060826_media_uploads/up.sql b/_migrations/2022-12-15-060826_media_uploads/up.sql index e78bd1f33e..98f599199a 100644 --- a/_migrations/2022-12-15-060826_media_uploads/up.sql +++ b/_migrations/2022-12-15-060826_media_uploads/up.sql @@ -60,6 +60,8 @@ CREATE TABLE media_uploads ( -- ========== UPLOAD, TRANSCODING, AND TRUNCATION DETAILS ========== + -- TODO(bt, 2022-12-20): Before landing, rename this field be better and indicate this + -- is a *directory*. Add a second field to contain the default file name. -- The directory this media is uploaded to will be exclusive for this file. -- Only this given record will live in this bucket, but the directory may include -- other transcodings or truncations. diff --git a/crates/lib/database_queries/src/mediators/firehose_publisher.rs b/crates/lib/database_queries/src/mediators/firehose_publisher.rs index 203554fafa..5cdff00101 100644 --- a/crates/lib/database_queries/src/mediators/firehose_publisher.rs +++ b/crates/lib/database_queries/src/mediators/firehose_publisher.rs @@ -6,12 +6,15 @@ use sqlx::error::Error::Database; use sqlx::mysql::MySqlQueryResult; use sqlx::{MySqlPool}; use std::sync::Arc; +use tokens::files::media_upload::MediaUploadToken; +use tokens::users::user::UserToken; +// TODO(bt, 2022-12-19): Convert this to a database 'enum'. Also, create an 'enums' package similar to 'tokens'. #[derive(Debug, Clone, Copy)] enum FirehoseEvent { UserSignUp, - // We don't publish for all badges (eg. early user signup) + // NB: We don't publish for all badges (eg. early user signup) UserBadgeGranted, TtsModelUploadStarted, @@ -27,6 +30,9 @@ enum FirehoseEvent { GenericDownloadStarted, GenericDownloadCompleted, + MediaUploaded, + + // TODO(bt, 2022-12-19): Are the following unused, merely planned (?) TwitterMention, TwitterRetweet, DiscordJoin, @@ -48,14 +54,17 @@ impl FirehoseEvent { FirehoseEvent::W2lTemplateUploadCompleted => "w2l_template_upload_completed", FirehoseEvent::W2lInferenceStarted => "w2l_inference_started", FirehoseEvent::W2lInferenceCompleted => "w2l_inference_completed", + FirehoseEvent::GenericDownloadStarted => "generic_download_started", + FirehoseEvent::GenericDownloadCompleted => "generic_download_completed", + FirehoseEvent::MediaUploaded => "media_uploaded", + + // Are the following unused (?) FirehoseEvent::TwitterMention => "twitter_mention", FirehoseEvent::TwitterRetweet => "twitter_retweet", FirehoseEvent::DiscordJoin => "discord_join", FirehoseEvent::DiscordMessage => "discord_message", FirehoseEvent::TwitchSubscribe => "twitch_subscribe", FirehoseEvent::TwitchFollow => "twitch_follow", - FirehoseEvent::GenericDownloadStarted => "generic_download_started", - FirehoseEvent::GenericDownloadCompleted => "generic_download_completed", } } } @@ -198,6 +207,16 @@ impl FirehosePublisher { Ok(()) } + pub async fn publish_media_uploaded(&self, maybe_user_token: Option<&UserToken>, upload_token: &MediaUploadToken) -> AnyhowResult<()> { + let _record_id = self.insert( + FirehoseEvent::MediaUploaded, + maybe_user_token.map(|u| u.as_str()), + Some(upload_token.as_str()), + Some(upload_token.as_str()), + ).await?; + Ok(()) + } + // ======================================================================= async fn insert( @@ -237,6 +256,8 @@ SET Ok(res) => { res.last_insert_id() }, + // TODO(bt, 2022-12-20): I've never richly handled database errors/error codes. + // I should revisit these in the future and consider some kind of middleware. Err(err) => { warn!("Insert record DB error: {:?}", err); diff --git a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs new file mode 100644 index 0000000000..50d12b8713 --- /dev/null +++ b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs @@ -0,0 +1,100 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::media_upload_type::MediaUploadType; +use sqlx::MySqlPool; +use reusable_types::db::payloads::MediaUploadDetails; +use tokens::files::media_upload::MediaUploadToken; +use tokens::users::user::UserToken; + +pub struct Args <'a> { + pub token: &'a MediaUploadToken, + pub uuid_idempotency_token: &'a str, + + pub media_type: MediaUploadType, + pub maybe_original_filename: Option<&'a str>, + pub original_file_size_bytes: u64, + pub original_duration_millis: u64, + pub maybe_original_mime_type: Option<&'a str>, + pub maybe_original_audio_encoding: Option<&'a str>, + pub maybe_original_video_encoding: Option<&'a str>, + pub maybe_original_frame_width: Option, + pub maybe_original_frame_height: Option, + pub checksum_sha2: &'a str, + + pub public_bucket_directory_full_path: &'a str, + pub extra_file_modification_info: MediaUploadDetails, + + pub maybe_creator_user_token: Option<&'a UserToken>, + pub maybe_creator_anonymous_visitor_token: Option<&'a str>, + pub creator_ip_address: &'a str, + pub creator_set_visibility: EntityVisibility, + + pub maybe_creator_synthetic_id: Option, + + pub mysql_pool: &'a MySqlPool, +} + +pub async fn insert_media_upload(args: Args<'_>) -> AnyhowResult { + + let query = sqlx::query!( + r#" +INSERT INTO media_uploads +SET + token = ?, + uuid_idempotency_token = ?, + + media_type = ?, + maybe_original_filename = ?, + original_file_size_bytes = ?, + original_duration_millis = ?, + maybe_original_mime_type = ?, + maybe_original_audio_encoding = ?, + maybe_original_video_encoding = ?, + maybe_original_frame_width = ?, + maybe_original_frame_height = ?, + checksum_sha2 = ?, + + public_bucket_directory_full_path = ?, + extra_file_modification_info = ?, + + maybe_creator_user_token = ?, + maybe_creator_anonymous_visitor_token = ?, + creator_ip_address = ?, + creator_set_visibility = ?, + + maybe_creator_synthetic_id = ? + "#, + args.token, + args.uuid_idempotency_token, + + args.media_type, + args.maybe_original_filename, + args.original_file_size_bytes, + args.original_duration_millis, + args.maybe_original_mime_type, + args.maybe_original_audio_encoding, + args.maybe_original_video_encoding, + args.maybe_original_frame_width, + args.maybe_original_frame_height, + args.checksum_sha2, + + args.public_bucket_directory_full_path, + "", // TODO: args.extra_file_modification_info, + + args.maybe_creator_user_token, + args.maybe_creator_anonymous_visitor_token, + args.creator_ip_address, + args.creator_set_visibility, + + args.maybe_creator_synthetic_id, + ); + + let query_result = query.execute(args.mysql_pool) + .await; + + match query_result { + Ok(res) => Ok(res.last_insert_id()), + Err(err) => Err(anyhow!("error inserting new media upload: {:?}", err)), + } +} diff --git a/crates/lib/database_queries/src/queries/media_uploads/mod.rs b/crates/lib/database_queries/src/queries/media_uploads/mod.rs new file mode 100644 index 0000000000..d8571ca39b --- /dev/null +++ b/crates/lib/database_queries/src/queries/media_uploads/mod.rs @@ -0,0 +1 @@ +pub mod insert_media_upload; \ No newline at end of file diff --git a/crates/lib/database_queries/src/queries/mod.rs b/crates/lib/database_queries/src/queries/mod.rs index ca02151cc0..b15d0be795 100644 --- a/crates/lib/database_queries/src/queries/mod.rs +++ b/crates/lib/database_queries/src/queries/mod.rs @@ -4,6 +4,7 @@ pub mod generic_download; pub mod generic_inference; pub mod health_check; pub mod ip_bans; +pub mod media_uploads; pub mod model_categories; pub mod stats; pub mod tts; diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs b/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs index 9d848bd0e3..a6c3f65cdf 100644 --- a/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs +++ b/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs @@ -9,9 +9,10 @@ use crate::helpers::boolean_converters::{nullable_i8_to_optional_bool, i8_to_boo use log::warn; use sqlx::MySql; use sqlx::pool::PoolConnection; +use tokens::users::user::UserToken; pub struct SessionUserRecord { - pub user_token: String, + pub user_token: String, // TODO(bt, 2022-12-20): Convert to strongly-typed `UserToken` pub username: String, pub display_name: String, @@ -65,6 +66,13 @@ pub struct SessionUserRecord { pub can_delete_users: bool, } +impl SessionUserRecord { + // TODO(bt, 2022-12-20): Convert all users of the bare record to using `UserToken`, then get rid of this method. + pub fn get_strongly_typed_user_token(&self) -> UserToken { + UserToken::new_from_str(&self.user_token) + } +} + pub async fn get_user_session_by_token( mysql_connection: &mut PoolConnection, session_token: &str, diff --git a/crates/lib/reusable_types/src/db/enums/media_upload_type.rs b/crates/lib/reusable_types/src/db/enums/media_upload_type.rs new file mode 100644 index 0000000000..c16c49e9a1 --- /dev/null +++ b/crates/lib/reusable_types/src/db/enums/media_upload_type.rs @@ -0,0 +1,53 @@ +/// Used in the `media_uploads` table. +/// +/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] +#[serde(rename_all = "lowercase")] +#[sqlx(rename_all = "lowercase")] +pub enum MediaUploadType { + Audio, + Video, +} + +/// NB: Legacy API for older code. +impl MediaUploadType { + pub fn to_str(&self) -> &'static str { + match self { + Self::Audio => "audio", + Self::Video => "video", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "audio" => Ok(Self::Audio), + "video" => Ok(Self::Video), + _ => Err(format!("invalid value: {:?}", value)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::db::enums::media_upload_type::MediaUploadType; + use crate::test_helpers::assert_serialization; + + #[test] + fn test_serialization() { + assert_serialization(MediaUploadType::Audio, "audio"); + assert_serialization(MediaUploadType::Video, "video"); + } + + #[test] + fn test_to_str() { + assert_eq!(MediaUploadType::Audio.to_str(), "audio"); + assert_eq!(MediaUploadType::Video.to_str(), "video"); + } + + #[test] + fn test_from_str() { + assert_eq!(MediaUploadType::from_str("audio").unwrap(), MediaUploadType::Audio); + assert_eq!(MediaUploadType::from_str("video").unwrap(), MediaUploadType::Video); + assert!(MediaUploadType::from_str("foo").is_err()); + } +} diff --git a/crates/lib/reusable_types/src/db/enums/mod.rs b/crates/lib/reusable_types/src/db/enums/mod.rs index 1e65a74f6b..351ce576b3 100644 --- a/crates/lib/reusable_types/src/db/enums/mod.rs +++ b/crates/lib/reusable_types/src/db/enums/mod.rs @@ -4,4 +4,5 @@ pub mod entity_visibility; pub mod generic_download_type; pub mod generic_inference_type; +pub mod media_upload_type; pub mod vocoder_type; diff --git a/crates/lib/reusable_types/src/db/payloads/media_upload_details.rs b/crates/lib/reusable_types/src/db/payloads/media_upload_details.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/lib/reusable_types/src/db/payloads/mod.rs b/crates/lib/reusable_types/src/db/payloads/mod.rs index e69de29bb2..8d819959a6 100644 --- a/crates/lib/reusable_types/src/db/payloads/mod.rs +++ b/crates/lib/reusable_types/src/db/payloads/mod.rs @@ -0,0 +1,5 @@ +pub mod media_upload_details; + +/// NB: All fields should be *optional*! +pub struct MediaUploadDetails { +} diff --git a/crates/lib/tokens/src/files/media_upload.rs b/crates/lib/tokens/src/files/media_upload.rs new file mode 100644 index 0000000000..542983a739 --- /dev/null +++ b/crates/lib/tokens/src/files/media_upload.rs @@ -0,0 +1,12 @@ +use crate::prefixes::EntityType; +use serde::Deserialize; +use serde::Serialize; +use std::fmt::Debug; + +/// The primary key for user media uploads (images, video, etc.) +#[derive(Clone, PartialEq, Eq, sqlx::Type, Debug, Serialize, Deserialize)] +#[sqlx(transparent)] +pub struct MediaUploadToken(String); + +impl_string_token!(MediaUploadToken); +impl_crockford_generator!(MediaUploadToken, 32usize, EntityType::DownloadJob, CrockfordLower); diff --git a/crates/lib/tokens/src/files/mod.rs b/crates/lib/tokens/src/files/mod.rs new file mode 100644 index 0000000000..740382ea60 --- /dev/null +++ b/crates/lib/tokens/src/files/mod.rs @@ -0,0 +1 @@ +pub mod media_upload; \ No newline at end of file diff --git a/crates/lib/tokens/src/lib.rs b/crates/lib/tokens/src/lib.rs index 7de58f9d9e..9495b78bb6 100644 --- a/crates/lib/tokens/src/lib.rs +++ b/crates/lib/tokens/src/lib.rs @@ -37,6 +37,7 @@ mod macros; pub (crate) mod prefixes; +pub mod files; pub mod jobs; pub mod users; pub mod voice_conversion; diff --git a/crates/lib/tokens/src/prefixes.rs b/crates/lib/tokens/src/prefixes.rs index 4d7566fd43..6508b68a82 100644 --- a/crates/lib/tokens/src/prefixes.rs +++ b/crates/lib/tokens/src/prefixes.rs @@ -15,6 +15,7 @@ use strum::EnumIter; pub(crate) enum EntityType { DownloadJob, InferenceJob, + MediaUpload, User, VoiceConversionModel, } @@ -25,6 +26,7 @@ impl EntityType { match self { Self::DownloadJob => "jdown_", // NB: Was "JGUP:" Self::InferenceJob => "jinf_", + Self::MediaUpload => "mu_", Self::User => "U:", // NB: Old-style prefix. Self::VoiceConversionModel => "voco_", } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs new file mode 100644 index 0000000000..c9d90a31ae --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs @@ -0,0 +1,57 @@ +use actix_multipart::Field; +use actix_multipart::Multipart; +use actix_web::web::BytesMut; +use container_common::anyhow_result::AnyhowResult; +use crate::http_server::web_utils::read_multipart_field_bytes::{checked_read_multipart_bytes, read_multipart_field_as_text}; +use futures::TryStreamExt; +use log::warn; + +pub struct UploadMediaRequest { + pub uuid_idempotency_token: Option, + pub file_name: Option, + pub file_bytes: Option, +} + +/// 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 { + let mut uuid_idempotency_token = None; + let mut file_bytes = None; + let mut file_name = None; + + while let Ok(Some(mut field)) = multipart_payload.try_next().await { + let mut field_name = None; + let mut field_filename = None; + + if let Some(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("file_bytes") => { + file_name = field_filename.clone(); + file_bytes = checked_read_multipart_bytes(&mut field).await + .map_err(|e| { + warn!("Error reading audio upload: {:}", e); + e + })?; + }, + _ => continue, + } + } + + Ok(UploadMediaRequest { + uuid_idempotency_token, + file_name, + file_bytes, + }) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/mod.rs new file mode 100644 index 0000000000..91cf062fc9 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/mod.rs @@ -0,0 +1,3 @@ +pub mod drain_multipart_request; +pub mod upload_audio; +pub mod upload_video; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs new file mode 100644 index 0000000000..412672b308 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -0,0 +1,187 @@ +use actix_http::Error; +use actix_http::http::header; +use actix_multipart::Multipart; +use actix_web::HttpResponseBuilder; +use actix_web::cookie::Cookie; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use config::bad_urls::is_bad_tts_model_download_url; +use crate::http_server::endpoints::media_uploads::drain_multipart_request::drain_multipart_request; +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::server_state::ServerState; +use crate::validations::model_uploads::validate_model_title; +use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use database_queries::column_types::record_visibility::RecordVisibility; +use database_queries::queries::media_uploads::insert_media_upload::{Args, insert_media_upload}; +use database_queries::tokens::Tokens; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use log::{info, warn, log}; +use regex::Regex; +use reusable_types::db::enums::entity_visibility::EntityVisibility; +use reusable_types::db::enums::generic_download_type::GenericDownloadType; +use reusable_types::db::enums::media_upload_type::MediaUploadType; +use reusable_types::db::payloads::MediaUploadDetails; +use sqlx::error::DatabaseError; +use sqlx::error::Error::Database; +use sqlx::mysql::MySqlDatabaseError; +use std::fmt; +use std::sync::Arc; +use tokens::files::media_upload::MediaUploadToken; + +#[derive(Serialize)] +pub struct UploadAudioSuccessResponse { + pub success: bool, + pub upload_token: MediaUploadToken, +} + +#[derive(Debug, Serialize)] +pub enum UploadAudioError { + BadInput(String), + NotAuthorized, + MustBeLoggedIn, + ServerError, + RateLimited, +} + +impl ResponseError for UploadAudioError { + fn status_code(&self) -> StatusCode { + match *self { + UploadAudioError::BadInput(_) => StatusCode::BAD_REQUEST, + UploadAudioError::NotAuthorized => StatusCode::UNAUTHORIZED, + UploadAudioError::MustBeLoggedIn => StatusCode::UNAUTHORIZED, + UploadAudioError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + UploadAudioError::RateLimited => StatusCode::TOO_MANY_REQUESTS, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for UploadAudioError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn upload_audio_handler( + http_request: HttpRequest, + server_state: web::Data>, + mut multipart_payload: Multipart, +) -> Result { + + // ==================== READ SESSION ==================== // + + 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); + UploadAudioError::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(UploadAudioError::NotAuthorized); + } + &server_state.redis_rate_limiters.logged_in + }, + }; + + if let Err(_err) = rate_limiter.rate_limit_request(&http_request) { + return Err(UploadAudioError::RateLimited); + } + + if let Err(_err) = server_state.redis_rate_limiters.model_upload.rate_limit_request(&http_request) { + return Err(UploadAudioError::RateLimited); + } + + // ==================== READ MULTIPART REQUEST ==================== // + + let upload_media_request = drain_multipart_request(multipart_payload) + .await + .map_err(|e| { + // TODO: Error handling could be nicer. + UploadAudioError::BadInput("bad request".to_string()) + })?; + + + let uuid_idempotency_token = upload_media_request.uuid_idempotency_token + .ok_or(UploadAudioError::BadInput("no uuid".to_string()))?; + + if let Err(reason) = validate_idempotency_token_format(&uuid_idempotency_token) { + return Err(UploadAudioError::BadInput(reason)); + } + + let creator_set_visibility = maybe_user_session + .as_ref() + .map(|user_session| user_session.preferred_tts_result_visibility) + .unwrap_or(RecordVisibility::Public); + + let ip_address = get_request_ip(&http_request); + + let maybe_user_token = maybe_user_session + .map(|session| session.get_strongly_typed_user_token()); + + let token = MediaUploadToken::generate(); + + let record_id = insert_media_upload(Args { + token: &token, + uuid_idempotency_token: &uuid_idempotency_token, + media_type: MediaUploadType::Audio, + maybe_original_filename: upload_media_request.file_name.as_deref(), + original_file_size_bytes: 0, + original_duration_millis: 0, + maybe_original_mime_type: None, + maybe_original_audio_encoding: None, + maybe_original_video_encoding: None, + maybe_original_frame_width: None, + maybe_original_frame_height: None, + checksum_sha2: "", + public_bucket_directory_full_path: "", + extra_file_modification_info: MediaUploadDetails {}, // TODO + maybe_creator_user_token: maybe_user_token.as_ref(), + maybe_creator_anonymous_visitor_token: None, + creator_ip_address: &ip_address, + creator_set_visibility: EntityVisibility::Public, // TODO: Combine these two types. + maybe_creator_synthetic_id: None, + mysql_pool: &server_state.mysql_pool, + }) + .await + .map_err(|err| { + warn!("New generic download creation DB error: {:?}", err); + UploadAudioError::ServerError + })?; + + info!("new media upload id: {}", record_id); + + server_state.firehose_publisher.publish_media_uploaded( + maybe_user_token.as_ref(), + &token) + .await + .map_err(|e| { + warn!("error publishing event: {:?}", e); + UploadAudioError::ServerError + })?; + + let response = UploadAudioSuccessResponse { + success: true, + upload_token: token, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| UploadAudioError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_video.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_video.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs index 40244a9277..aaa72c4f1c 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs @@ -5,6 +5,7 @@ pub mod events; pub mod flags; pub mod investor_demo; pub mod leaderboard; +pub mod media_uploads; pub mod misc; pub mod moderation; pub mod service; diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index 80698ac7fb..c9f4db34b9 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -21,6 +21,7 @@ use crate::http_server::endpoints::flags::design_refresh_flag::enable_design_ref use crate::http_server::endpoints::investor_demo::disable_demo_mode_handler::disable_demo_mode_handler; use crate::http_server::endpoints::investor_demo::enable_demo_mode_handler::enable_demo_mode_handler; use crate::http_server::endpoints::leaderboard::get_leaderboard::leaderboard_handler; +use crate::http_server::endpoints::media_uploads::upload_audio::upload_audio_handler; use crate::http_server::endpoints::misc::default_route_404::default_route_404; use crate::http_server::endpoints::misc::detect_locale_handler::detect_locale_handler; use crate::http_server::endpoints::misc::enable_alpha_easy_handler::enable_alpha_easy_handler; @@ -37,6 +38,7 @@ use crate::http_server::endpoints::moderation::ip_bans::list_ip_bans::list_ip_ba use crate::http_server::endpoints::moderation::jobs::get_tts_inference_queue_count::get_tts_inference_queue_count_handler; use crate::http_server::endpoints::moderation::jobs::get_w2l_inference_queue_count::get_w2l_inference_queue_count_handler; use crate::http_server::endpoints::moderation::jobs::kill_tts_inference_jobs::kill_tts_inference_jobs_handler; +use crate::http_server::endpoints::moderation::stats::get_on_prem_worker_stats::get_on_prem_worker_stats_handler; use crate::http_server::endpoints::moderation::stats::get_voice_count_stats::get_voice_count_stats_handler; use crate::http_server::endpoints::moderation::user_bans::ban_user::ban_user_handler; use crate::http_server::endpoints::moderation::user_bans::list_banned_users::list_banned_users_handler; @@ -97,7 +99,6 @@ use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_t use users_component::default_routes::add_suggested_api_v1_account_creation_and_session_routes; use users_component::endpoints::edit_profile_handler::edit_profile_handler; use users_component::endpoints::get_profile_handler::get_profile_handler; -use crate::http_server::endpoints::moderation::stats::get_on_prem_worker_stats::get_on_prem_worker_stats_handler; pub fn add_routes (app: App) -> App where @@ -122,7 +123,8 @@ pub fn add_routes (app: App) -> App app = add_twitch_routes(app); /* /twitch */ // TODO: MAYBE TEMPORARY app = add_investor_demo_routes(app); /* /demo_mode */ // TODO: DEFINITELY TEMPORARY app = add_flag_routes(app); /* /flag */ - app = add_desktop_app_routes(app); /* /api/v1/vc/... */ + app = add_desktop_app_routes(app); /* /v1/vc/... */ + app = add_media_upload_routes(app); /* /v1/media_upload/... */ // From components app = add_suggested_api_v1_account_creation_and_session_routes(app); // /create_account, /session, /login, /logout @@ -776,6 +778,27 @@ fn add_desktop_app_routes (app: App) -> App ) } +// ==================== MEDIA UPLOAD ROUTES ==================== + +fn add_media_upload_routes (app: App) -> App + where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app.service(web::scope("/v1/media_upload") + .service(web::resource("/upload_audio") + .route(web::post().to(upload_audio_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) +} + // ==================== TWITCH ROUTES ==================== // TODO: Maybe move these into an "oauth-gateway" type http service. From e7fc93de86a11f27f3bdc12a9696b30fe4b6fc03 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 20 Dec 2022 01:44:26 -0500 Subject: [PATCH 18/54] Mild refactor: tokens, reusable_types under types/ directory --- Cargo.toml | 6 ++++-- crates/lib/database_queries/Cargo.toml | 4 ++-- .../src/queries/media_uploads/insert_media_upload.rs | 2 +- crates/lib/http_server_common/Cargo.toml | 2 +- crates/lib/url_config/Cargo.toml | 2 +- crates/plugins/billing/Cargo.toml | 2 +- crates/plugins/users/Cargo.toml | 4 ++-- crates/service/job/download_job/Cargo.toml | 2 +- crates/service/web/image_generation_service/Cargo.toml | 2 +- crates/service/web/storyteller_web/Cargo.toml | 4 ++-- crates/{lib => types}/reusable_types/Cargo.toml | 0 .../{lib => types}/reusable_types/src/achievements/mod.rs | 0 .../reusable_types/src/achievements/user_badge.rs | 0 .../reusable_types/src/achievements/user_badge_type.rs | 0 .../reusable_types/src/db/enums/entity_visibility.rs | 0 .../reusable_types/src/db/enums/generic_download_type.rs | 0 .../reusable_types/src/db/enums/generic_inference_type.rs | 0 .../reusable_types/src/db/enums/media_upload_type.rs | 0 crates/{lib => types}/reusable_types/src/db/enums/mod.rs | 0 .../reusable_types/src/db/enums/vocoder_type.rs | 0 crates/{lib => types}/reusable_types/src/db/mod.rs | 0 .../reusable_types/src/db/payloads/media_upload_details.rs | 0 crates/{lib => types}/reusable_types/src/db/payloads/mod.rs | 0 crates/{lib => types}/reusable_types/src/lib.rs | 0 .../{lib => types}/reusable_types/src/server_environment.rs | 0 crates/{lib => types}/reusable_types/src/stripe/mod.rs | 0 .../reusable_types/src/stripe/stripe_recurring_interval.rs | 0 .../reusable_types/src/stripe/stripe_subscription_status.rs | 0 crates/{lib => types}/reusable_types/src/test_helpers.rs | 0 crates/{lib => types}/tokens/Cargo.toml | 0 crates/{lib => types}/tokens/src/files/media_upload.rs | 0 crates/{lib => types}/tokens/src/files/mod.rs | 0 crates/{lib => types}/tokens/src/jobs/download.rs | 0 crates/{lib => types}/tokens/src/jobs/inference.rs | 0 crates/{lib => types}/tokens/src/jobs/mod.rs | 0 crates/{lib => types}/tokens/src/lib.rs | 0 crates/{lib => types}/tokens/src/macros.rs | 0 crates/{lib => types}/tokens/src/prefixes.rs | 0 crates/{lib => types}/tokens/src/users/mod.rs | 0 crates/{lib => types}/tokens/src/users/user.rs | 0 crates/{lib => types}/tokens/src/voice_conversion/mod.rs | 0 crates/{lib => types}/tokens/src/voice_conversion/model.rs | 0 crates/{lib => types}/tokens/tests/user_token_tests.rs | 0 43 files changed, 16 insertions(+), 14 deletions(-) rename crates/{lib => types}/reusable_types/Cargo.toml (100%) rename crates/{lib => types}/reusable_types/src/achievements/mod.rs (100%) rename crates/{lib => types}/reusable_types/src/achievements/user_badge.rs (100%) rename crates/{lib => types}/reusable_types/src/achievements/user_badge_type.rs (100%) rename crates/{lib => types}/reusable_types/src/db/enums/entity_visibility.rs (100%) rename crates/{lib => types}/reusable_types/src/db/enums/generic_download_type.rs (100%) rename crates/{lib => types}/reusable_types/src/db/enums/generic_inference_type.rs (100%) rename crates/{lib => types}/reusable_types/src/db/enums/media_upload_type.rs (100%) rename crates/{lib => types}/reusable_types/src/db/enums/mod.rs (100%) rename crates/{lib => types}/reusable_types/src/db/enums/vocoder_type.rs (100%) rename crates/{lib => types}/reusable_types/src/db/mod.rs (100%) rename crates/{lib => types}/reusable_types/src/db/payloads/media_upload_details.rs (100%) rename crates/{lib => types}/reusable_types/src/db/payloads/mod.rs (100%) rename crates/{lib => types}/reusable_types/src/lib.rs (100%) rename crates/{lib => types}/reusable_types/src/server_environment.rs (100%) rename crates/{lib => types}/reusable_types/src/stripe/mod.rs (100%) rename crates/{lib => types}/reusable_types/src/stripe/stripe_recurring_interval.rs (100%) rename crates/{lib => types}/reusable_types/src/stripe/stripe_subscription_status.rs (100%) rename crates/{lib => types}/reusable_types/src/test_helpers.rs (100%) rename crates/{lib => types}/tokens/Cargo.toml (100%) rename crates/{lib => types}/tokens/src/files/media_upload.rs (100%) rename crates/{lib => types}/tokens/src/files/mod.rs (100%) rename crates/{lib => types}/tokens/src/jobs/download.rs (100%) rename crates/{lib => types}/tokens/src/jobs/inference.rs (100%) rename crates/{lib => types}/tokens/src/jobs/mod.rs (100%) rename crates/{lib => types}/tokens/src/lib.rs (100%) rename crates/{lib => types}/tokens/src/macros.rs (100%) rename crates/{lib => types}/tokens/src/prefixes.rs (100%) rename crates/{lib => types}/tokens/src/users/mod.rs (100%) rename crates/{lib => types}/tokens/src/users/user.rs (100%) rename crates/{lib => types}/tokens/src/voice_conversion/mod.rs (100%) rename crates/{lib => types}/tokens/src/voice_conversion/model.rs (100%) rename crates/{lib => types}/tokens/tests/user_token_tests.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index ac4ddb632a..1ee9bd9aa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,15 +19,17 @@ members = [ "crates/lib/jobs_common", "crates/lib/memory_caching", "crates/lib/redis_common", - "crates/lib/reusable_types", "crates/lib/storage_buckets_common", "crates/lib/subprocess_common", - "crates/lib/tokens", "crates/lib/tts_common", "crates/lib/twitch_common", "crates/lib/url_config", "crates/lib/user_input_common", + # Types: common strongly-typed tokens, ids, payloads, etc. + "crates/types/reusable_types", + "crates/types/tokens", + # Services: API servers, jobs, workers, etc. "crates/service/job/download_job", "crates/service/job/inference_job", diff --git a/crates/lib/database_queries/Cargo.toml b/crates/lib/database_queries/Cargo.toml index 9ce708b265..281565ac7b 100644 --- a/crates/lib/database_queries/Cargo.toml +++ b/crates/lib/database_queries/Cargo.toml @@ -17,8 +17,8 @@ path = "src/lib.rs" # Internal config = { path = "../config" } container_common = { path = "../container_common" } -reusable_types = { path = "../reusable_types" } -tokens = { path = "../tokens" } +reusable_types = { path = "../../types/reusable_types" } +tokens = { path = "../../types/tokens" } # External anyhow = "1.0.40" diff --git a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs index 50d12b8713..a823fe497a 100644 --- a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs +++ b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs @@ -2,8 +2,8 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; use reusable_types::db::enums::entity_visibility::EntityVisibility; use reusable_types::db::enums::media_upload_type::MediaUploadType; -use sqlx::MySqlPool; use reusable_types::db::payloads::MediaUploadDetails; +use sqlx::MySqlPool; use tokens::files::media_upload::MediaUploadToken; use tokens::users::user::UserToken; diff --git a/crates/lib/http_server_common/Cargo.toml b/crates/lib/http_server_common/Cargo.toml index ae6e251573..c570de11d5 100644 --- a/crates/lib/http_server_common/Cargo.toml +++ b/crates/lib/http_server_common/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" # Internal container_common = { path = "../container_common" } -reusable_types = { path = "../reusable_types" } +reusable_types = { path = "../../types/reusable_types" } # External anyhow = "1.0.40" diff --git a/crates/lib/url_config/Cargo.toml b/crates/lib/url_config/Cargo.toml index 534cbb5ff3..88bad19e0e 100644 --- a/crates/lib/url_config/Cargo.toml +++ b/crates/lib/url_config/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" # Internal http_server_common = { path = "../../lib/http_server_common" } -reusable_types = { path = "../../lib/reusable_types" } +reusable_types = { path = "../../types/reusable_types" } # External actix-web = "4.0.0-beta.10" diff --git a/crates/plugins/billing/Cargo.toml b/crates/plugins/billing/Cargo.toml index 6b090e3079..ed92473329 100644 --- a/crates/plugins/billing/Cargo.toml +++ b/crates/plugins/billing/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" container_common = { path = "../../lib/container_common" } database_queries = { path = "../../lib/database_queries" } http_server_common = { path = "../../lib/http_server_common" } -reusable_types = { path = "../../lib/reusable_types" } +reusable_types = { path = "../../types/reusable_types" } url_config = { path = "../../lib/url_config" } # External diff --git a/crates/plugins/users/Cargo.toml b/crates/plugins/users/Cargo.toml index 215efd96c1..7e674def8c 100644 --- a/crates/plugins/users/Cargo.toml +++ b/crates/plugins/users/Cargo.toml @@ -18,8 +18,8 @@ path = "src/lib.rs" container_common = { path = "../../lib/container_common" } database_queries = { path = "../../lib/database_queries" } http_server_common = { path = "../../lib/http_server_common" } -reusable_types = { path = "../../lib/reusable_types" } -tokens = { path = "../../lib/tokens" } +reusable_types = { path = "../../types/reusable_types" } +tokens = { path = "../../types/tokens" } user_input_common = { path = "../../lib/user_input_common" } # External diff --git a/crates/service/job/download_job/Cargo.toml b/crates/service/job/download_job/Cargo.toml index bd0becbfd6..372b9d5726 100644 --- a/crates/service/job/download_job/Cargo.toml +++ b/crates/service/job/download_job/Cargo.toml @@ -20,7 +20,7 @@ container_common = { path = "../../../lib/container_common" } database_queries = { path = "../../../lib/database_queries" } google_drive_common = { path = "../../../lib/google_drive_common" } jobs_common = { path = "../../../lib/jobs_common" } -reusable_types = { path = "../../../lib/reusable_types" } +reusable_types = { path = "../../../types/reusable_types" } storage_buckets_common = { path = "../../../lib/storage_buckets_common" } subprocess_common = { path = "../../../lib/subprocess_common" } tts_common = { path = "../../../lib/tts_common" } diff --git a/crates/service/web/image_generation_service/Cargo.toml b/crates/service/web/image_generation_service/Cargo.toml index 4b64b2c8e7..9cca2972e2 100644 --- a/crates/service/web/image_generation_service/Cargo.toml +++ b/crates/service/web/image_generation_service/Cargo.toml @@ -24,7 +24,7 @@ http_server_common = { path = "../../../lib/http_server_common" } jobs_common = { path = "../../../lib/jobs_common" } memory_caching = { path = "../../../lib/memory_caching" } redis_common = { path = "../../../lib/redis_common" } -reusable_types = { path = "../../../lib/reusable_types" } +reusable_types = { path = "../../../types/reusable_types" } storage_buckets_common = { path = "../../../lib/storage_buckets_common" } tts_common = { path = "../../../lib/tts_common" } twitch_common = { path = "../../../lib/twitch_common" } diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index 2d31a374f3..cbb52362a3 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -24,9 +24,9 @@ http_server_common = { path = "../../../lib/http_server_common" } jobs_common = { path = "../../../lib/jobs_common" } memory_caching = { path = "../../../lib/memory_caching" } redis_common = { path = "../../../lib/redis_common" } -reusable_types = { path = "../../../lib/reusable_types" } +reusable_types = { path = "../../../types/reusable_types" } storage_buckets_common = { path = "../../../lib/storage_buckets_common" } -tokens = { path = "../../../lib/tokens" } +tokens = { path = "../../../types/tokens" } tts_common = { path = "../../../lib/tts_common" } twitch_common = { path = "../../../lib/twitch_common" } url_config = { path = "../../../lib/url_config" } diff --git a/crates/lib/reusable_types/Cargo.toml b/crates/types/reusable_types/Cargo.toml similarity index 100% rename from crates/lib/reusable_types/Cargo.toml rename to crates/types/reusable_types/Cargo.toml diff --git a/crates/lib/reusable_types/src/achievements/mod.rs b/crates/types/reusable_types/src/achievements/mod.rs similarity index 100% rename from crates/lib/reusable_types/src/achievements/mod.rs rename to crates/types/reusable_types/src/achievements/mod.rs diff --git a/crates/lib/reusable_types/src/achievements/user_badge.rs b/crates/types/reusable_types/src/achievements/user_badge.rs similarity index 100% rename from crates/lib/reusable_types/src/achievements/user_badge.rs rename to crates/types/reusable_types/src/achievements/user_badge.rs diff --git a/crates/lib/reusable_types/src/achievements/user_badge_type.rs b/crates/types/reusable_types/src/achievements/user_badge_type.rs similarity index 100% rename from crates/lib/reusable_types/src/achievements/user_badge_type.rs rename to crates/types/reusable_types/src/achievements/user_badge_type.rs diff --git a/crates/lib/reusable_types/src/db/enums/entity_visibility.rs b/crates/types/reusable_types/src/db/enums/entity_visibility.rs similarity index 100% rename from crates/lib/reusable_types/src/db/enums/entity_visibility.rs rename to crates/types/reusable_types/src/db/enums/entity_visibility.rs diff --git a/crates/lib/reusable_types/src/db/enums/generic_download_type.rs b/crates/types/reusable_types/src/db/enums/generic_download_type.rs similarity index 100% rename from crates/lib/reusable_types/src/db/enums/generic_download_type.rs rename to crates/types/reusable_types/src/db/enums/generic_download_type.rs diff --git a/crates/lib/reusable_types/src/db/enums/generic_inference_type.rs b/crates/types/reusable_types/src/db/enums/generic_inference_type.rs similarity index 100% rename from crates/lib/reusable_types/src/db/enums/generic_inference_type.rs rename to crates/types/reusable_types/src/db/enums/generic_inference_type.rs diff --git a/crates/lib/reusable_types/src/db/enums/media_upload_type.rs b/crates/types/reusable_types/src/db/enums/media_upload_type.rs similarity index 100% rename from crates/lib/reusable_types/src/db/enums/media_upload_type.rs rename to crates/types/reusable_types/src/db/enums/media_upload_type.rs diff --git a/crates/lib/reusable_types/src/db/enums/mod.rs b/crates/types/reusable_types/src/db/enums/mod.rs similarity index 100% rename from crates/lib/reusable_types/src/db/enums/mod.rs rename to crates/types/reusable_types/src/db/enums/mod.rs diff --git a/crates/lib/reusable_types/src/db/enums/vocoder_type.rs b/crates/types/reusable_types/src/db/enums/vocoder_type.rs similarity index 100% rename from crates/lib/reusable_types/src/db/enums/vocoder_type.rs rename to crates/types/reusable_types/src/db/enums/vocoder_type.rs diff --git a/crates/lib/reusable_types/src/db/mod.rs b/crates/types/reusable_types/src/db/mod.rs similarity index 100% rename from crates/lib/reusable_types/src/db/mod.rs rename to crates/types/reusable_types/src/db/mod.rs diff --git a/crates/lib/reusable_types/src/db/payloads/media_upload_details.rs b/crates/types/reusable_types/src/db/payloads/media_upload_details.rs similarity index 100% rename from crates/lib/reusable_types/src/db/payloads/media_upload_details.rs rename to crates/types/reusable_types/src/db/payloads/media_upload_details.rs diff --git a/crates/lib/reusable_types/src/db/payloads/mod.rs b/crates/types/reusable_types/src/db/payloads/mod.rs similarity index 100% rename from crates/lib/reusable_types/src/db/payloads/mod.rs rename to crates/types/reusable_types/src/db/payloads/mod.rs diff --git a/crates/lib/reusable_types/src/lib.rs b/crates/types/reusable_types/src/lib.rs similarity index 100% rename from crates/lib/reusable_types/src/lib.rs rename to crates/types/reusable_types/src/lib.rs diff --git a/crates/lib/reusable_types/src/server_environment.rs b/crates/types/reusable_types/src/server_environment.rs similarity index 100% rename from crates/lib/reusable_types/src/server_environment.rs rename to crates/types/reusable_types/src/server_environment.rs diff --git a/crates/lib/reusable_types/src/stripe/mod.rs b/crates/types/reusable_types/src/stripe/mod.rs similarity index 100% rename from crates/lib/reusable_types/src/stripe/mod.rs rename to crates/types/reusable_types/src/stripe/mod.rs diff --git a/crates/lib/reusable_types/src/stripe/stripe_recurring_interval.rs b/crates/types/reusable_types/src/stripe/stripe_recurring_interval.rs similarity index 100% rename from crates/lib/reusable_types/src/stripe/stripe_recurring_interval.rs rename to crates/types/reusable_types/src/stripe/stripe_recurring_interval.rs diff --git a/crates/lib/reusable_types/src/stripe/stripe_subscription_status.rs b/crates/types/reusable_types/src/stripe/stripe_subscription_status.rs similarity index 100% rename from crates/lib/reusable_types/src/stripe/stripe_subscription_status.rs rename to crates/types/reusable_types/src/stripe/stripe_subscription_status.rs diff --git a/crates/lib/reusable_types/src/test_helpers.rs b/crates/types/reusable_types/src/test_helpers.rs similarity index 100% rename from crates/lib/reusable_types/src/test_helpers.rs rename to crates/types/reusable_types/src/test_helpers.rs diff --git a/crates/lib/tokens/Cargo.toml b/crates/types/tokens/Cargo.toml similarity index 100% rename from crates/lib/tokens/Cargo.toml rename to crates/types/tokens/Cargo.toml diff --git a/crates/lib/tokens/src/files/media_upload.rs b/crates/types/tokens/src/files/media_upload.rs similarity index 100% rename from crates/lib/tokens/src/files/media_upload.rs rename to crates/types/tokens/src/files/media_upload.rs diff --git a/crates/lib/tokens/src/files/mod.rs b/crates/types/tokens/src/files/mod.rs similarity index 100% rename from crates/lib/tokens/src/files/mod.rs rename to crates/types/tokens/src/files/mod.rs diff --git a/crates/lib/tokens/src/jobs/download.rs b/crates/types/tokens/src/jobs/download.rs similarity index 100% rename from crates/lib/tokens/src/jobs/download.rs rename to crates/types/tokens/src/jobs/download.rs diff --git a/crates/lib/tokens/src/jobs/inference.rs b/crates/types/tokens/src/jobs/inference.rs similarity index 100% rename from crates/lib/tokens/src/jobs/inference.rs rename to crates/types/tokens/src/jobs/inference.rs diff --git a/crates/lib/tokens/src/jobs/mod.rs b/crates/types/tokens/src/jobs/mod.rs similarity index 100% rename from crates/lib/tokens/src/jobs/mod.rs rename to crates/types/tokens/src/jobs/mod.rs diff --git a/crates/lib/tokens/src/lib.rs b/crates/types/tokens/src/lib.rs similarity index 100% rename from crates/lib/tokens/src/lib.rs rename to crates/types/tokens/src/lib.rs diff --git a/crates/lib/tokens/src/macros.rs b/crates/types/tokens/src/macros.rs similarity index 100% rename from crates/lib/tokens/src/macros.rs rename to crates/types/tokens/src/macros.rs diff --git a/crates/lib/tokens/src/prefixes.rs b/crates/types/tokens/src/prefixes.rs similarity index 100% rename from crates/lib/tokens/src/prefixes.rs rename to crates/types/tokens/src/prefixes.rs diff --git a/crates/lib/tokens/src/users/mod.rs b/crates/types/tokens/src/users/mod.rs similarity index 100% rename from crates/lib/tokens/src/users/mod.rs rename to crates/types/tokens/src/users/mod.rs diff --git a/crates/lib/tokens/src/users/user.rs b/crates/types/tokens/src/users/user.rs similarity index 100% rename from crates/lib/tokens/src/users/user.rs rename to crates/types/tokens/src/users/user.rs diff --git a/crates/lib/tokens/src/voice_conversion/mod.rs b/crates/types/tokens/src/voice_conversion/mod.rs similarity index 100% rename from crates/lib/tokens/src/voice_conversion/mod.rs rename to crates/types/tokens/src/voice_conversion/mod.rs diff --git a/crates/lib/tokens/src/voice_conversion/model.rs b/crates/types/tokens/src/voice_conversion/model.rs similarity index 100% rename from crates/lib/tokens/src/voice_conversion/model.rs rename to crates/types/tokens/src/voice_conversion/model.rs diff --git a/crates/lib/tokens/tests/user_token_tests.rs b/crates/types/tokens/tests/user_token_tests.rs similarity index 100% rename from crates/lib/tokens/tests/user_token_tests.rs rename to crates/types/tokens/tests/user_token_tests.rs From d08d3cd5431563e707ec106891e1585c0a547644 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 20 Dec 2022 02:13:26 -0500 Subject: [PATCH 19/54] New 'enums' crate for db "enum-like" fields, composite keys, etc. This should be useful for: - Actual enum fields - Enum-like char/varchar fields - Composite keys, polymorphic record behavior, etc. This should also define how these values are returned over the HTTP API --- Cargo.lock | 16 +++- Cargo.toml | 1 + crates/lib/database_queries/Cargo.toml | 1 + .../media_uploads/insert_media_upload.rs | 4 +- .../get_user_session_by_token.rs | 19 ++++ crates/service/web/storyteller_web/Cargo.toml | 1 + .../endpoints/media_uploads/upload_audio.rs | 9 +- crates/types/enums/Cargo.toml | 33 +++++++ crates/types/enums/src/core/mod.rs | 1 + crates/types/enums/src/core/visibility.rs | 89 +++++++++++++++++++ crates/types/enums/src/lib.rs | 26 ++++++ crates/types/enums/src/test_helpers.rs | 16 ++++ crates/types/tokens/Cargo.toml | 3 - crates/types/tokens/src/lib.rs | 2 + 14 files changed, 210 insertions(+), 11 deletions(-) create mode 100644 crates/types/enums/Cargo.toml create mode 100644 crates/types/enums/src/core/mod.rs create mode 100644 crates/types/enums/src/core/visibility.rs create mode 100644 crates/types/enums/src/lib.rs create mode 100644 crates/types/enums/src/test_helpers.rs diff --git a/Cargo.lock b/Cargo.lock index 438c6a6ddd..98bfced272 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1466,6 +1466,7 @@ dependencies = [ "config", "container_common", "dotenv", + "enums", "log", "once_cell", "regex", @@ -1691,6 +1692,19 @@ dependencies = [ "syn", ] +[[package]] +name = "enums" +version = "0.0.1" +dependencies = [ + "serde", + "serde_derive", + "serde_json", + "sqlx", + "sqlx-core", + "strum", + "toml", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -4514,6 +4528,7 @@ dependencies = [ "diesel", "dotenv", "easyenv", + "enums", "futures 0.1.31", "futures 0.3.17", "futures-util", @@ -4843,7 +4858,6 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" name = "tokens" version = "0.0.1" dependencies = [ - "anyhow", "rand 0.8.4", "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index 1ee9bd9aa6..4c70aa854a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "crates/lib/user_input_common", # Types: common strongly-typed tokens, ids, payloads, etc. + "crates/types/enums", "crates/types/reusable_types", "crates/types/tokens", diff --git a/crates/lib/database_queries/Cargo.toml b/crates/lib/database_queries/Cargo.toml index 281565ac7b..1e9854bd09 100644 --- a/crates/lib/database_queries/Cargo.toml +++ b/crates/lib/database_queries/Cargo.toml @@ -17,6 +17,7 @@ path = "src/lib.rs" # Internal config = { path = "../config" } container_common = { path = "../container_common" } +enums = { path = "../../types/enums" } reusable_types = { path = "../../types/reusable_types" } tokens = { path = "../../types/tokens" } diff --git a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs index a823fe497a..5997a291ca 100644 --- a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs +++ b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; -use reusable_types::db::enums::entity_visibility::EntityVisibility; +use enums::core::visibility::Visibility; use reusable_types::db::enums::media_upload_type::MediaUploadType; use reusable_types::db::payloads::MediaUploadDetails; use sqlx::MySqlPool; @@ -28,7 +28,7 @@ pub struct Args <'a> { pub maybe_creator_user_token: Option<&'a UserToken>, pub maybe_creator_anonymous_visitor_token: Option<&'a str>, pub creator_ip_address: &'a str, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub maybe_creator_synthetic_id: Option, diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs b/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs index a6c3f65cdf..8017409f17 100644 --- a/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs +++ b/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs @@ -9,6 +9,7 @@ use crate::helpers::boolean_converters::{nullable_i8_to_optional_bool, i8_to_boo use log::warn; use sqlx::MySql; use sqlx::pool::PoolConnection; +use enums::core::visibility::Visibility; use tokens::users::user::UserToken; pub struct SessionUserRecord { @@ -71,6 +72,24 @@ impl SessionUserRecord { pub fn get_strongly_typed_user_token(&self) -> UserToken { UserToken::new_from_str(&self.user_token) } + + // TODO(bt, 2022-12-20): Expunge RecordVisibility and EntityVisibility from the codebase. + pub fn get_preferred_tts_result_visibility(&self) -> Visibility { + match self.preferred_tts_result_visibility { + RecordVisibility::Public => Visibility::Public, + RecordVisibility::Hidden => Visibility::Hidden, + RecordVisibility::Private => Visibility::Private, + } + } + + // TODO(bt, 2022-12-20): Expunge RecordVisibility and EntityVisibility from the codebase. + pub fn get_preferred_w2l_result_visibility(&self) -> Visibility { + match self.preferred_w2l_result_visibility { + RecordVisibility::Public => Visibility::Public, + RecordVisibility::Hidden => Visibility::Hidden, + RecordVisibility::Private => Visibility::Private, + } + } } pub async fn get_user_session_by_token( diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index cbb52362a3..b591342d1b 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -19,6 +19,7 @@ path = "src/main.rs" config = { path = "../../../lib/config" } container_common = { path = "../../../lib/container_common" } database_queries = { path = "../../../lib/database_queries" } +enums = { path = "../../../types/enums" } google_drive_common = { path = "../../../lib/google_drive_common" } http_server_common = { path = "../../../lib/http_server_common" } jobs_common = { path = "../../../lib/jobs_common" } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index 412672b308..78c640f35d 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -14,12 +14,11 @@ use crate::validations::model_uploads::validate_model_title; use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::media_uploads::insert_media_upload::{Args, insert_media_upload}; -use database_queries::tokens::Tokens; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; use regex::Regex; -use reusable_types::db::enums::entity_visibility::EntityVisibility; use reusable_types::db::enums::generic_download_type::GenericDownloadType; use reusable_types::db::enums::media_upload_type::MediaUploadType; use reusable_types::db::payloads::MediaUploadDetails; @@ -124,8 +123,8 @@ pub async fn upload_audio_handler( let creator_set_visibility = maybe_user_session .as_ref() - .map(|user_session| user_session.preferred_tts_result_visibility) - .unwrap_or(RecordVisibility::Public); + .map(|user_session| user_session.get_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); @@ -152,7 +151,7 @@ pub async fn upload_audio_handler( maybe_creator_user_token: maybe_user_token.as_ref(), maybe_creator_anonymous_visitor_token: None, creator_ip_address: &ip_address, - creator_set_visibility: EntityVisibility::Public, // TODO: Combine these two types. + creator_set_visibility, maybe_creator_synthetic_id: None, mysql_pool: &server_state.mysql_pool, }) diff --git a/crates/types/enums/Cargo.toml b/crates/types/enums/Cargo.toml new file mode 100644 index 0000000000..c325f41cce --- /dev/null +++ b/crates/types/enums/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "enums" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +name = "enums" +path = "src/lib.rs" + +[dependencies] +# Serialization +serde = "1.0.125" +serde_derive = "1.0.125" +serde_json = "1.0.64" +toml = "0.5.8" + +# MySql Database +# "offline" supports typesafe builds without a live database +# "rustls" doesn't appear to work with IP addresses, per: +# https://github.com/launchbadge/sqlx/issues/1095#issuecomment-798050828 +#sqlx = { version = "0.4", features = [ "mysql", "runtime-tokio-native-tls", "offline", "chrono" ] } +sqlx = { version = "0.5", features = [ "mysql", "runtime-actix-native-tls", "offline", "chrono" ] } +sqlx-core = { version = "0.5.7" } + +[dev-dependencies] + +# Enum utilities +strum = { version = "0.24", features = ["derive"] } diff --git a/crates/types/enums/src/core/mod.rs b/crates/types/enums/src/core/mod.rs new file mode 100644 index 0000000000..21b9312b2f --- /dev/null +++ b/crates/types/enums/src/core/mod.rs @@ -0,0 +1 @@ +pub mod visibility; \ No newline at end of file diff --git a/crates/types/enums/src/core/visibility.rs b/crates/types/enums/src/core/visibility.rs new file mode 100644 index 0000000000..9e6aa0d30d --- /dev/null +++ b/crates/types/enums/src/core/visibility.rs @@ -0,0 +1,89 @@ +/// Visibility +/// +/// Used in various database tables (as enums! careful!) and the HTTP API to convey +/// how the associated entity should be made visible to the public. +/// +/// To use this in a query, the query must have type annotations. +/// See: https://www.gitmemory.com/issue/launchbadge/sqlx/1241/847154375 +/// eg. preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::core::visibility::Visibility` +/// +/// See also: https://docs.rs/sqlx/0.4.0-beta.1/sqlx/trait.Type.html +/// +/// *DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY!* +/// +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] +#[serde(rename_all = "lowercase")] +#[sqlx(rename_all = "lowercase")] +pub enum Visibility { + /// Public entities are able to be listed in public lists. + /// It does not mean that they necessarily will be (eg. they could be "mod unapproved" or deleted). + Public, + /// Hidden entities are not shown in public lists, but the URL to them may be given out freely. + /// They are available to non-logged-in users as long as they have the URL. + Hidden, + /// Private entities should only be available to the creator, a list of approved users, and + /// website moderation staff. + Private, + + // TODO(bt, 2022-12-20): We need a "Shared" option where users can share it with a specified group. + // This should perhaps be its own type, eg. VisibilityV2., so that we don't use it in tables that + // have not yet been migrated to this scheme. +} + +impl Default for Visibility { + fn default() -> Self { Self::Public } +} + +/// NB: Legacy API for older code. +impl Visibility { + pub fn to_str(&self) -> &'static str { + match self { + Self::Public => "public", + Self::Hidden => "hidden", + Self::Private => "private", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "public" => Ok(Self::Public), + "hidden" => Ok(Self::Hidden), + "private" => Ok(Self::Private), + _ => Err(format!("invalid value: {:?}", value)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::core::visibility::Visibility; + use crate::test_helpers::assert_serialization; + + #[test] + fn test_default() { + assert_eq!(Visibility::default(), Visibility::Public); + + } + + #[test] + fn test_serialization() { + assert_serialization(Visibility::Public, "public"); + assert_serialization(Visibility::Hidden, "hidden"); + assert_serialization(Visibility::Private, "private"); + } + + #[test] + fn test_to_str() { + assert_eq!(Visibility::Public.to_str(), "public"); + assert_eq!(Visibility::Hidden.to_str(), "hidden"); + assert_eq!(Visibility::Private.to_str(), "private"); + } + + #[test] + fn test_from_str() { + assert_eq!(Visibility::from_str("public").unwrap(), Visibility::Public); + assert_eq!(Visibility::from_str("hidden").unwrap(), Visibility::Hidden); + assert_eq!(Visibility::from_str("private").unwrap(), Visibility::Private); + assert!(Visibility::from_str("foo").is_err()); + } +} diff --git a/crates/types/enums/src/lib.rs b/crates/types/enums/src/lib.rs new file mode 100644 index 0000000000..7f35b53561 --- /dev/null +++ b/crates/types/enums/src/lib.rs @@ -0,0 +1,26 @@ +//! enums +//! +//! The purpose of this library is to have a strongly-typed MySQL enum-type wrapper. +//! This should also work for CHAR/VARCHAR fields that work similarly to enums (typically +//! as part of a composite key) +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +#[macro_use] extern crate serde_derive; + +#[cfg(test)] pub mod test_helpers; + +pub mod core; diff --git a/crates/types/enums/src/test_helpers.rs b/crates/types/enums/src/test_helpers.rs new file mode 100644 index 0000000000..6d16bfddd5 --- /dev/null +++ b/crates/types/enums/src/test_helpers.rs @@ -0,0 +1,16 @@ +//! Only imported in tests + +use serde::Serialize; + +pub fn to_json(t: T) -> String { + toml::to_string(&t) + .expect("serialization error") + .replace("\"", "") // JSON values are quoted, so we remove quotes +} + +/// Assert that the Serialize is represented by the expected string value. +/// This is useful for ensuring stability of serialization. +pub fn assert_serialization(t: T, expected: &str) { + // TODO: See if there's a way to check the sqlx serialization + assert_eq!(&to_json(t), expected); +} diff --git a/crates/types/tokens/Cargo.toml b/crates/types/tokens/Cargo.toml index c6626ba4e2..5ca9d9b935 100644 --- a/crates/types/tokens/Cargo.toml +++ b/crates/types/tokens/Cargo.toml @@ -13,9 +13,6 @@ name = "tokens" path = "src/lib.rs" [dependencies] -# Errors -anyhow = "1.0.40" - # Serialization serde = "1.0.125" serde_derive = "1.0.125" diff --git a/crates/types/tokens/src/lib.rs b/crates/types/tokens/src/lib.rs index 9495b78bb6..7453a53f88 100644 --- a/crates/types/tokens/src/lib.rs +++ b/crates/types/tokens/src/lib.rs @@ -1,7 +1,9 @@ //! tokens +//! //! The purpose of this library is to have a strongly-typed primary/foreign key system. //! Every Database or Redis key will have a type here. //! Well known keys will have short identifiers (eg. "user" is prefixed with "U:") +//! // Never allow these #![forbid(private_in_public)] From 68f2e46540ada5c969f74bc1e18791a5e1fb5535 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 20 Dec 2022 02:27:00 -0500 Subject: [PATCH 20/54] more tests on enum types --- crates/types/enums/src/core/visibility.rs | 51 +++++++++++++++++++ crates/types/tokens/tests/user_token_tests.rs | 4 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/crates/types/enums/src/core/visibility.rs b/crates/types/enums/src/core/visibility.rs index 9e6aa0d30d..6689fe9b94 100644 --- a/crates/types/enums/src/core/visibility.rs +++ b/crates/types/enums/src/core/visibility.rs @@ -86,4 +86,55 @@ mod tests { assert_eq!(Visibility::from_str("private").unwrap(), Visibility::Private); assert!(Visibility::from_str("foo").is_err()); } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct CompositeType { + visibility: Visibility, + string: String, + } + + mod serde_serialization { + use crate::core::visibility::Visibility; + use super::CompositeType; + + #[test] + fn serialize() { + let expected = "\"public\"".to_string(); // NB: Quoted + + assert_eq!(expected, toml::to_string(&Visibility::Public).unwrap()); + + // Just to show this serializes the same as a string + assert_eq!(expected, toml::to_string("public").unwrap()); + } + + #[test] + fn nested_serialize() { + let value = CompositeType { visibility: Visibility::Hidden, string: "bar".to_string() }; + let expected = r#"{"visibility":"hidden","string":"bar"}"#.to_string(); + assert_eq!(expected, serde_json::to_string(&value).unwrap()); + } + } + + mod serde_deserialization { + use crate::core::visibility::Visibility; + use super::CompositeType; + + #[test] + fn deserialize() { + let payload = "\"private\""; // NB: Quoted + let value: Visibility = serde_json::from_str(payload).unwrap(); + assert_eq!(value, Visibility::Private); + } + + #[test] + fn nested_deserialize() { + let payload = r#"{"visibility":"hidden","string":"bar"}"#.to_string(); + let expected = CompositeType { + visibility: Visibility::Hidden, + string: "bar".to_string(), + }; + + assert_eq!(expected, serde_json::from_str::(&payload).unwrap()); + } + } } diff --git a/crates/types/tokens/tests/user_token_tests.rs b/crates/types/tokens/tests/user_token_tests.rs index 8fb10e61a3..47a01de03d 100644 --- a/crates/types/tokens/tests/user_token_tests.rs +++ b/crates/types/tokens/tests/user_token_tests.rs @@ -88,7 +88,7 @@ mod deserialization { use tokens::users::user::UserToken; #[test] - fn test_deserialize_trait() { + fn deserialize() { let payload = "\"U:foo\""; // NB: Quoted let expected = "U:foo".to_string(); @@ -101,7 +101,7 @@ mod deserialization { } #[test] - fn test_nested_deserialize_trait() { + fn nested_deserialize() { let payload = r#"{"user_token":"U:foo","string":"bar"}"#.to_string(); let expected = CompositeType { user_token: UserToken("U:foo".to_string()), From 66bb76e60f8eaf040b64caa85e71720e5b4542ce Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 20 Dec 2022 02:50:17 -0500 Subject: [PATCH 21/54] Goodbye, 'EntityVisibility' --- Cargo.lock | 2 + .../list_available_generic_download_jobs.rs | 8 +-- .../web/get_generic_download_job_status.rs | 1 - .../web/insert_generic_download_job.rs | 4 +- .../list_available_generic_inference_jobs.rs | 8 +-- .../web/insert_generic_inference_job.rs | 4 +- .../insert_tts_model_from_download_job.rs | 4 +- .../get_user_profile_by_username.rs | 14 ++-- .../src/queries/vocoder/get_vocoder_model.rs | 10 +-- .../queries/vocoder/insert_vocoder_model.rs | 4 +- ...oice_conversion_model_from_download_job.rs | 4 +- crates/plugins/users/Cargo.toml | 1 + .../src/endpoints/get_profile_handler.rs | 6 +- crates/service/job/download_job/Cargo.toml | 1 + .../job_types/softvc/process_softvc_model.rs | 4 +- .../tacotron/process_tacotron_model.rs | 4 +- .../download_job/enqueue_generic_download.rs | 6 +- .../endpoints/vocoders/get_vocoder.rs | 4 +- .../src/db/enums/entity_visibility.rs | 66 ------------------- .../types/reusable_types/src/db/enums/mod.rs | 1 - 20 files changed, 46 insertions(+), 110 deletions(-) delete mode 100644 crates/types/reusable_types/src/db/enums/entity_visibility.rs diff --git a/Cargo.lock b/Cargo.lock index 98bfced272..1996857525 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1624,6 +1624,7 @@ dependencies = [ "database_queries", "dotenv", "easyenv", + "enums", "google_drive_common", "hostname", "hyper 0.14.14", @@ -5569,6 +5570,7 @@ dependencies = [ "container_common", "database_queries", "dotenv", + "enums", "hmac 0.10.1", "http_server_common", "jwt", diff --git a/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs b/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs index 9eeb944c39..d1cb02cf9d 100644 --- a/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs +++ b/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs @@ -4,7 +4,7 @@ use container_common::anyhow_result::AnyhowResult; use crate::column_types::job_status::JobStatus; use crate::queries::generic_download::job::_keys::GenericDownloadJobId; use crate::tokens::Tokens; -use reusable_types::db::enums::entity_visibility::EntityVisibility; +use enums::core::visibility::Visibility; use reusable_types::db::enums::generic_download_type::GenericDownloadType; use sqlx::MySqlPool; use std::path::Path; @@ -18,7 +18,7 @@ pub struct AvailableDownloadJob { pub creator_user_token: String, pub creator_ip_address: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub download_type: GenericDownloadType, pub download_url: String, @@ -45,7 +45,7 @@ SELECT creator_user_token, creator_ip_address, - creator_set_visibility as `creator_set_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, download_type as `download_type: reusable_types::db::enums::generic_download_type::GenericDownloadType`, download_url, @@ -108,7 +108,7 @@ struct AvailableDownloadJobRawInternal { pub creator_user_token: String, pub creator_ip_address: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub download_type: GenericDownloadType, pub download_url: String, diff --git a/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs b/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs index 30e31960ec..2361a2da00 100644 --- a/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs +++ b/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs @@ -2,7 +2,6 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use log::warn; -use reusable_types::db::enums::entity_visibility::EntityVisibility; use reusable_types::db::enums::generic_download_type::GenericDownloadType; use sqlx::MySqlPool; use tokens::jobs::download::DownloadJobToken; diff --git a/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs b/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs index 08d485dc1a..f7d02a15ff 100644 --- a/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs +++ b/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; -use reusable_types::db::enums::entity_visibility::EntityVisibility; +use enums::core::visibility::Visibility; use reusable_types::db::enums::generic_download_type::GenericDownloadType; use sqlx::MySqlPool; use tokens::jobs::download::DownloadJobToken; @@ -13,7 +13,7 @@ pub struct Args <'a> { pub title: &'a str, pub creator_user_token: &'a str, pub creator_ip_address: &'a str, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub mysql_pool: &'a MySqlPool, } diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs b/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs index 57aa3a3353..bc4fb8afe0 100644 --- a/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs +++ b/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs @@ -4,7 +4,7 @@ use container_common::anyhow_result::AnyhowResult; use crate::column_types::job_status::JobStatus; use crate::helpers::boolean_converters::i8_to_bool; use crate::queries::generic_inference::job::_keys::GenericInferenceJobId; -use reusable_types::db::enums::entity_visibility::EntityVisibility; +use enums::core::visibility::Visibility; use reusable_types::db::enums::generic_inference_type::GenericInferenceType; use sqlx::MySqlPool; use std::path::Path; @@ -25,7 +25,7 @@ pub struct AvailableInferenceJob { // User information to propagate downstream pub maybe_creator_user_token: Option, pub creator_ip_address: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, // Job information pub status: JobStatus, @@ -58,7 +58,7 @@ SELECT maybe_creator_user_token, creator_ip_address, - creator_set_visibility as `creator_set_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, status as `status: crate::column_types::job_status::JobStatus`, @@ -135,7 +135,7 @@ struct AvailableInferenceJobRawInternal { // User information to propagate downstream pub maybe_creator_user_token: Option, pub creator_ip_address: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, // Job information pub status: JobStatus, diff --git a/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs b/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs index 94da5b75d2..eadb6e79fa 100644 --- a/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs +++ b/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; -use reusable_types::db::enums::entity_visibility::EntityVisibility; +use enums::core::visibility::Visibility; use reusable_types::db::enums::generic_inference_type::GenericInferenceType; use sqlx::MySqlPool; use tokens::jobs::inference::InferenceJobToken; @@ -16,7 +16,7 @@ pub struct Args <'a> { pub maybe_creator_user_token: Option<&'a str>, pub creator_ip_address: &'a str, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub mysql_pool: &'a MySqlPool, } diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs b/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs index a65d0d756f..00daa6d73e 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs @@ -3,7 +3,7 @@ use chrono::Utc; use container_common::anyhow_result::AnyhowResult; use crate::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use crate::tokens::Tokens; -use reusable_types::db::enums::entity_visibility::EntityVisibility; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; use std::path::Path; @@ -16,7 +16,7 @@ pub struct Args<'a, P: AsRef> { pub creator_user_token: &'a str, pub creator_ip_address: &'a str, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub private_bucket_hash: &'a str, pub private_bucket_object_name: P, diff --git a/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs b/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs index 9d60218cd7..f740834ae8 100644 --- a/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs +++ b/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs @@ -2,8 +2,8 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use crate::helpers::boolean_converters::i8_to_bool; +use enums::core::visibility::Visibility; use log::{info, warn, log}; -use reusable_types::db::enums::entity_visibility::EntityVisibility; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; @@ -38,8 +38,8 @@ pub struct UserProfileResult { pub website_url: Option, // Preferences; NB: included for get_profile_handler legacy reasons - pub preferred_tts_result_visibility: EntityVisibility, - pub preferred_w2l_result_visibility: EntityVisibility, + pub preferred_tts_result_visibility: Visibility, + pub preferred_w2l_result_visibility: Visibility, pub created_at: DateTime, @@ -75,8 +75,8 @@ struct RawUserProfileRecord { created_at: DateTime, // Preferences; NB: included for get_profile_handler legacy reasons - preferred_tts_result_visibility: EntityVisibility, - preferred_w2l_result_visibility: EntityVisibility, + preferred_tts_result_visibility: Visibility, + preferred_w2l_result_visibility: Visibility, // Mod fields is_banned: i8, @@ -109,8 +109,8 @@ SELECT profile_rendered_html, user_role_slug, disable_gravatar, - preferred_tts_result_visibility as `preferred_tts_result_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, - preferred_w2l_result_visibility as `preferred_w2l_result_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, + preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::core::visibility::Visibility`, + preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::core::visibility::Visibility`, discord_username, twitch_username, twitter_username, diff --git a/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs b/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs index b5244801b5..aa22ace7bc 100644 --- a/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs +++ b/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs @@ -7,8 +7,8 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use crate::helpers::boolean_converters::i8_to_bool; +use enums::core::visibility::Visibility; use log::warn; -use reusable_types::db::enums::entity_visibility::EntityVisibility; use reusable_types::db::enums::vocoder_type::VocoderType; use sqlx::MySqlPool; @@ -31,7 +31,7 @@ pub struct VocoderModelRecord { pub creator_display_name: String, pub creator_gravatar_hash: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub created_at: DateTime, pub updated_at: DateTime, @@ -137,7 +137,7 @@ SELECT vocoder.is_mod_disabled_from_author_use, vocoder.is_mod_author_editing_locked, - vocoder.creator_set_visibility as `creator_set_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, + vocoder.creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, vocoder.created_at, vocoder.updated_at, @@ -185,7 +185,7 @@ SELECT vocoder.is_mod_disabled_from_author_use, vocoder.is_mod_author_editing_locked, - vocoder.creator_set_visibility as `creator_set_visibility: reusable_types::db::enums::entity_visibility::EntityVisibility`, + vocoder.creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, vocoder.created_at, vocoder.updated_at, @@ -224,7 +224,7 @@ struct InternalVocoderModelRecordRaw { pub creator_display_name: String, pub creator_gravatar_hash: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub created_at: DateTime, pub updated_at: DateTime, diff --git a/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs b/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs index 41b97722b2..bc3cb4f8ba 100644 --- a/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs +++ b/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs @@ -6,7 +6,7 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; use crate::tokens::Tokens; -use reusable_types::db::enums::entity_visibility::EntityVisibility; +use enums::core::visibility::Visibility; use reusable_types::db::enums::vocoder_type::VocoderType; use sqlx::MySqlPool; use std::path::Path; @@ -22,7 +22,7 @@ pub struct Args<'a, P: AsRef> { pub creator_user_token: &'a str, pub creator_ip_address: &'a str, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub private_bucket_hash: &'a str, pub private_bucket_object_name: P, diff --git a/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs b/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs index 9c70ad24be..d6bf18996b 100644 --- a/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs +++ b/crates/lib/database_queries/src/queries/voice_conversion/voice_conversion_models/insert_voice_conversion_model_from_download_job.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; use chrono::Utc; use container_common::anyhow_result::AnyhowResult; -use reusable_types::db::enums::entity_visibility::EntityVisibility; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; use std::path::Path; use tokens::voice_conversion::model::VoiceConversionModelToken; @@ -15,7 +15,7 @@ pub struct Args<'a, P: AsRef> { pub creator_user_token: &'a str, pub creator_ip_address: &'a str, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub private_bucket_hash: &'a str, pub private_bucket_object_name: P, diff --git a/crates/plugins/users/Cargo.toml b/crates/plugins/users/Cargo.toml index 7e674def8c..a072e348bf 100644 --- a/crates/plugins/users/Cargo.toml +++ b/crates/plugins/users/Cargo.toml @@ -17,6 +17,7 @@ path = "src/lib.rs" # Internal container_common = { path = "../../lib/container_common" } database_queries = { path = "../../lib/database_queries" } +enums = { path = "../../types/enums" } http_server_common = { path = "../../lib/http_server_common" } reusable_types = { path = "../../types/reusable_types" } tokens = { path = "../../types/tokens" } diff --git a/crates/plugins/users/src/endpoints/get_profile_handler.rs b/crates/plugins/users/src/endpoints/get_profile_handler.rs index e768701924..7120d372ed 100644 --- a/crates/plugins/users/src/endpoints/get_profile_handler.rs +++ b/crates/plugins/users/src/endpoints/get_profile_handler.rs @@ -12,11 +12,11 @@ use crate::utils::session_checker::SessionChecker; use database_queries::queries::users::user_badges::list_user_badges::UserBadgeForList; use database_queries::queries::users::user_badges::list_user_badges::list_user_badges; use database_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username_from_connection; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_header_optional::get_request_header_optional; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use http_server_common::util::timer::MultiBenchmarkingTimer; use log::warn; -use reusable_types::db::enums::entity_visibility::EntityVisibility; use sqlx::MySqlPool; use std::fmt; use tokens::users::user::UserToken; @@ -34,8 +34,8 @@ pub struct UserProfileRecordForResponse { pub profile_rendered_html: String, pub user_role_slug: String, pub disable_gravatar: bool, - pub preferred_tts_result_visibility: EntityVisibility, - pub preferred_w2l_result_visibility: EntityVisibility, + pub preferred_tts_result_visibility: Visibility, + pub preferred_w2l_result_visibility: Visibility, pub discord_username: Option, pub twitch_username: Option, pub twitter_username: Option, diff --git a/crates/service/job/download_job/Cargo.toml b/crates/service/job/download_job/Cargo.toml index 372b9d5726..39196b6692 100644 --- a/crates/service/job/download_job/Cargo.toml +++ b/crates/service/job/download_job/Cargo.toml @@ -18,6 +18,7 @@ path = "src/main.rs" config = { path = "../../../lib/config" } container_common = { path = "../../../lib/container_common" } database_queries = { path = "../../../lib/database_queries" } +enums = { path = "../../../types/enums" } google_drive_common = { path = "../../../lib/google_drive_common" } jobs_common = { path = "../../../lib/jobs_common" } reusable_types = { path = "../../../types/reusable_types" } diff --git a/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs index 1910929bae..bd133e5ee1 100644 --- a/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs +++ b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs @@ -8,9 +8,9 @@ use crate::JobState; use crate::job_steps::job_results::JobResults; use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use database_queries::queries::voice_conversion::voice_conversion_models::insert_voice_conversion_model_from_download_job::{Args, insert_voice_conversion_model_from_download_job}; +use enums::core::visibility::Visibility; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; -use reusable_types::db::enums::entity_visibility::EntityVisibility; use std::fs::File; use std::io::Read; use std::path::PathBuf; @@ -101,7 +101,7 @@ pub async fn process_softvc_model<'a, 'b>( file_size_bytes: file_metadata.file_size_bytes, creator_user_token: &job.creator_user_token, creator_ip_address: &job.creator_ip_address, - creator_set_visibility: EntityVisibility::Public, // TODO: All models default to public at start + creator_set_visibility: Visibility::Public, // TODO: All models default to public at start private_bucket_hash: &private_bucket_hash, private_bucket_object_name: &model_bucket_path, mysql_pool: &job_state.mysql_pool, diff --git a/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs b/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs index 5f9773a1ef..0306733d08 100644 --- a/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs +++ b/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs @@ -9,9 +9,9 @@ use crate::job_steps::job_results::JobResults; use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job::insert_tts_model_from_download_job; use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job; +use enums::core::visibility::Visibility; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; -use reusable_types::db::enums::entity_visibility::EntityVisibility; use std::fs::File; use std::io::Read; use std::path::PathBuf; @@ -103,7 +103,7 @@ pub async fn process_tacotron_model<'a, 'b>( file_size_bytes: file_metadata.file_size_bytes, creator_user_token: &job.creator_user_token, creator_ip_address: &job.creator_ip_address, - creator_set_visibility: EntityVisibility::Public, // TODO: All models default to public at start + creator_set_visibility: Visibility::Public, // TODO: All models default to public at start private_bucket_hash: &private_bucket_hash, private_bucket_object_name: &model_bucket_path, mysql_pool: &job_state.mysql_pool, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs index eadf802447..0ea1f440f9 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs @@ -12,11 +12,11 @@ use crate::validations::model_uploads::validate_model_title; use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; use database_queries::queries::generic_download::web::insert_generic_download_job::{Args, insert_generic_download_job}; use database_queries::tokens::Tokens; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; use regex::Regex; -use reusable_types::db::enums::entity_visibility::EntityVisibility; use reusable_types::db::enums::generic_download_type::GenericDownloadType; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; @@ -35,7 +35,7 @@ pub struct EnqueueGenericDownloadRequest { /// Not all upload types will have an associated visibility, so supplying this is optional. /// If an upload type supports it and it is not supplied, assume a reasonable default or user /// setting. - creator_set_visibility: Option, + creator_set_visibility: Option, } #[derive(Serialize)] @@ -107,7 +107,7 @@ pub async fn enqueue_generic_download_handler( let download_url = request.download_url.trim().to_string(); let creator_set_visibility = request.creator_set_visibility - .unwrap_or(EntityVisibility::Public); + .unwrap_or(Visibility::Public); if let Err(reason) = validate_idempotency_token_format(&uuid) { return Err(EnqueueGenericDownloadError::BadInput(reason)); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs index 33cafb9655..97fd1642b2 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs @@ -11,8 +11,8 @@ use chrono::{DateTime, Utc}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use database_queries::queries::vocoder::get_vocoder_model::get_vocoder_model_by_token; +use enums::core::visibility::Visibility; use log::warn; -use reusable_types::db::enums::entity_visibility::EntityVisibility; use reusable_types::db::enums::vocoder_type::VocoderType; use std::fmt; use std::sync::Arc; @@ -49,7 +49,7 @@ pub struct Vocoder { pub creator_display_name: String, pub creator_gravatar_hash: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub created_at: DateTime, pub updated_at: DateTime, diff --git a/crates/types/reusable_types/src/db/enums/entity_visibility.rs b/crates/types/reusable_types/src/db/enums/entity_visibility.rs deleted file mode 100644 index 5823899add..0000000000 --- a/crates/types/reusable_types/src/db/enums/entity_visibility.rs +++ /dev/null @@ -1,66 +0,0 @@ -/// Used in various database tables (as enums! careful!) and the HTTP API to convey -/// how the associated entity should be made visible to the public. -/// -/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] -#[serde(rename_all = "lowercase")] -#[sqlx(rename_all = "lowercase")] -pub enum EntityVisibility { - /// Public entities are able to be listed in public lists. - /// It does not mean that they necessarily will be (eg. they could be "mod unapproved" or deleted). - Public, - /// Hidden entities are not shown in public lists, but the URL to them may be given out freely. - /// They are available to non-logged-in users as long as they have the URL. - Hidden, - /// Private entities should only be available to the creator, a list of approved users, and - /// website moderation staff. - Private, -} - -/// NB: Legacy API for older code. -impl EntityVisibility { - pub fn to_str(&self) -> &'static str { - match self { - Self::Public => "public", - Self::Hidden => "hidden", - Self::Private => "private", - } - } - - pub fn from_str(value: &str) -> Result { - match value { - "public" => Ok(Self::Public), - "hidden" => Ok(Self::Hidden), - "private" => Ok(Self::Private), - _ => Err(format!("invalid value: {:?}", value)), - } - } -} - -#[cfg(test)] -mod tests { - use crate::db::enums::entity_visibility::EntityVisibility; - use crate::test_helpers::assert_serialization; - - #[test] - fn test_serialization() { - assert_serialization(EntityVisibility::Public, "public"); - assert_serialization(EntityVisibility::Hidden, "hidden"); - assert_serialization(EntityVisibility::Private, "private"); - } - - #[test] - fn test_to_str() { - assert_eq!(EntityVisibility::Public.to_str(), "public"); - assert_eq!(EntityVisibility::Hidden.to_str(), "hidden"); - assert_eq!(EntityVisibility::Private.to_str(), "private"); - } - - #[test] - fn test_from_str() { - assert_eq!(EntityVisibility::from_str("public").unwrap(), EntityVisibility::Public); - assert_eq!(EntityVisibility::from_str("hidden").unwrap(), EntityVisibility::Hidden); - assert_eq!(EntityVisibility::from_str("private").unwrap(), EntityVisibility::Private); - assert!(EntityVisibility::from_str("foo").is_err()); - } -} diff --git a/crates/types/reusable_types/src/db/enums/mod.rs b/crates/types/reusable_types/src/db/enums/mod.rs index 351ce576b3..226cd89c3c 100644 --- a/crates/types/reusable_types/src/db/enums/mod.rs +++ b/crates/types/reusable_types/src/db/enums/mod.rs @@ -1,7 +1,6 @@ //! This is meant to handle `enum` or `enum`-like columns in the database. //! These are not Rust-like enums (sum types) that can have internal payloads. -pub mod entity_visibility; pub mod generic_download_type; pub mod generic_inference_type; pub mod media_upload_type; From 31bb03d9d35fb964a0f67b0027ad7104099d2f92 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 20 Dec 2022 03:37:59 -0500 Subject: [PATCH 22/54] Sayonara, RecordVisibility --- .../database_queries/src/column_types/mod.rs | 1 - .../src/column_types/record_visibility.rs | 39 ------------------- .../list_available_tts_inference_jobs.rs | 12 +++--- .../tts/tts_models/edit_tts_model_details.rs | 6 +-- .../edit_tts_model_moderator_details.rs | 8 ++-- .../queries/tts/tts_models/get_tts_model.rs | 8 ++-- .../list_tts_results_query_builder.rs | 17 ++++---- .../tts/tts_results/query_tts_result.rs | 19 ++++----- .../get_user_session_by_token.rs | 33 ++++------------ .../w2l_inference_job_queries.rs | 6 +-- ...ist_w2l_inference_results_query_builder.rs | 21 +++++----- .../w2l/w2l_results/query_w2l_result.rs | 18 +++++---- .../w2l/w2l_templates/get_w2l_template.rs | 19 ++++----- .../src/endpoints/edit_profile_handler.rs | 10 ++--- .../users/src/utils/user_session_extended.rs | 6 +-- .../endpoints/media_uploads/upload_audio.rs | 3 +- .../endpoints/tts/edit_tts_model.rs | 6 +-- .../endpoints/tts/edit_tts_result.rs | 28 ++++++------- .../endpoints/tts/enqueue_infer_tts.rs | 31 +++++++-------- .../endpoints/tts/get_tts_model.rs | 4 +- .../endpoints/w2l/edit_w2l_result.rs | 28 ++++++------- .../endpoints/w2l/edit_w2l_template.rs | 6 +-- .../endpoints/w2l/enqueue_infer_w2l.rs | 27 ++++++------- .../w2l/enqueue_infer_w2l_with_uploads.rs | 38 +++++++++--------- 24 files changed, 166 insertions(+), 228 deletions(-) delete mode 100644 crates/lib/database_queries/src/column_types/record_visibility.rs diff --git a/crates/lib/database_queries/src/column_types/mod.rs b/crates/lib/database_queries/src/column_types/mod.rs index 9b54f0c4a4..a0ee4f37e6 100644 --- a/crates/lib/database_queries/src/column_types/mod.rs +++ b/crates/lib/database_queries/src/column_types/mod.rs @@ -1,5 +1,4 @@ pub mod job_status; -pub mod record_visibility; pub mod tts_model_type; pub mod twitch_event_category; pub mod vocoder_type; diff --git a/crates/lib/database_queries/src/column_types/record_visibility.rs b/crates/lib/database_queries/src/column_types/record_visibility.rs deleted file mode 100644 index 2b647db433..0000000000 --- a/crates/lib/database_queries/src/column_types/record_visibility.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! These are columns where users can control the visibility of their data. - -use anyhow::anyhow; -use container_common::anyhow_result::AnyhowResult; - -/// To use this in a query, the query must have type annotations. -/// See: https://www.gitmemory.com/issue/launchbadge/sqlx/1241/847154375 -/// eg. preferred_tts_result_visibility as `preferred_tts_result_visibility: crate::column_types::record_visibility::RecordVisibility` -#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, sqlx::Type)] -#[serde(rename_all = "lowercase")] -#[sqlx(rename_all = "lowercase")] -pub enum RecordVisibility { - Public, - Hidden, - Private, -} - -impl Default for RecordVisibility { - fn default() -> Self { Self::Public } -} - -impl RecordVisibility { - pub fn to_str(&self) -> &'static str { - match self { - RecordVisibility::Public => "public", - RecordVisibility::Hidden => "hidden", - RecordVisibility::Private => "private", - } - } - - pub fn from_str(record_visibility: &str) -> AnyhowResult { - match record_visibility { - "public" => Ok(RecordVisibility::Public), - "hidden" => Ok(RecordVisibility::Hidden), - "private" => Ok(RecordVisibility::Private), - _ => Err(anyhow!("invalid value: {:?}", record_visibility)), - } - } -} diff --git a/crates/lib/database_queries/src/queries/tts/tts_inference_jobs/list_available_tts_inference_jobs.rs b/crates/lib/database_queries/src/queries/tts/tts_inference_jobs/list_available_tts_inference_jobs.rs index 449f539301..d05ed468cc 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_inference_jobs/list_available_tts_inference_jobs.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_inference_jobs/list_available_tts_inference_jobs.rs @@ -6,9 +6,9 @@ use chrono::{Utc, DateTime}; use container_common::anyhow_result::AnyhowResult; use crate::column_types::job_status::JobStatus; -use crate::column_types::record_visibility::RecordVisibility; use crate::helpers::boolean_converters::i8_to_bool; use crate::queries::tts::tts_inference_jobs::_keys::TtsInferenceJobId; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; /// table: tts_inference_jobs @@ -29,7 +29,7 @@ pub struct AvailableTtsInferenceJob { pub creator_ip_address: String, pub maybe_creator_user_token: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_from_api: bool, pub is_for_twitch: bool, @@ -75,7 +75,7 @@ SELECT creator_ip_address, maybe_creator_user_token, - creator_set_visibility as `creator_set_visibility: crate::column_types::record_visibility::RecordVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, is_from_api, is_for_twitch, @@ -124,7 +124,7 @@ SELECT creator_ip_address, maybe_creator_user_token, - creator_set_visibility as `creator_set_visibility: crate::column_types::record_visibility::RecordVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, is_from_api, is_for_twitch, @@ -216,7 +216,7 @@ SELECT creator_ip_address, maybe_creator_user_token, - creator_set_visibility as `creator_set_visibility: crate::column_types::record_visibility::RecordVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, is_from_api, is_for_twitch, @@ -294,7 +294,7 @@ struct AvailableTtsInferenceJobRawInternal { pub creator_ip_address: String, pub maybe_creator_user_token: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_from_api: i8, pub is_for_twitch: i8, diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_details.rs b/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_details.rs index 3d850e2720..e11bf06396 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_details.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_details.rs @@ -1,5 +1,5 @@ -use crate::column_types::record_visibility::RecordVisibility; use crate::column_types::vocoder_type::VocoderType; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; use sqlx::mysql::MySqlQueryResult; use sqlx; @@ -12,7 +12,7 @@ pub async fn edit_tts_model_details_as_author( description_html: Option<&str>, ietf_language_tag: &str, ietf_primary_language_subtag: &str, - creator_set_visibility: RecordVisibility, + creator_set_visibility: Visibility, maybe_default_pretrained_vocoder: Option, maybe_custom_vocoder_token: Option<&str>, text_pipeline_type: Option<&str>, @@ -62,7 +62,7 @@ pub async fn edit_tts_model_details_as_mod( description_html: Option<&str>, ietf_language_tag: &str, ietf_primary_language_subtag: &str, - creator_set_visibility: RecordVisibility, + creator_set_visibility: Visibility, maybe_default_pretrained_vocoder: Option, maybe_custom_vocoder_token: Option<&str>, text_pipeline_type: Option<&str>, diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_moderator_details.rs b/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_moderator_details.rs index 484995077c..f19c37df94 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_moderator_details.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_moderator_details.rs @@ -1,6 +1,8 @@ -use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; -use crate::column_types::vocoder_type::VocoderType; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use sqlx::MySqlPool; use sqlx; use sqlx::mysql::MySqlQueryResult; diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model.rs b/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model.rs index cae563b19c..fae6592cd6 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model.rs @@ -6,9 +6,9 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::column_types::vocoder_type::VocoderType; use crate::helpers::boolean_converters::i8_to_bool; +use enums::core::visibility::Visibility; use log::warn; use sqlx::pool::PoolConnection; use sqlx::{MySql, MySqlPool}; @@ -44,7 +44,7 @@ pub struct TtsModelRecord { pub maybe_suggested_unique_bot_command: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_locked_from_use: bool, pub is_locked_from_user_modification: bool, @@ -152,8 +152,8 @@ pub async fn get_tts_model_by_token_using_connection( is_front_page_featured: i8_to_bool(model.is_front_page_featured), is_twitch_featured: i8_to_bool(model.is_twitch_featured), maybe_suggested_unique_bot_command: model.maybe_suggested_unique_bot_command, - creator_set_visibility: RecordVisibility::from_str(&model.creator_set_visibility) - .unwrap_or(RecordVisibility::Public), + creator_set_visibility: Visibility::from_str(&model.creator_set_visibility) + .unwrap_or(Visibility::Public), is_locked_from_use: i8_to_bool(model.is_locked_from_use), is_locked_from_user_modification: i8_to_bool(model.is_locked_from_user_modification), created_at: model.created_at, diff --git a/crates/lib/database_queries/src/queries/tts/tts_results/list_tts_results_query_builder.rs b/crates/lib/database_queries/src/queries/tts/tts_results/list_tts_results_query_builder.rs index a60b5bc44e..87762c8c6f 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_results/list_tts_results_query_builder.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_results/list_tts_results_query_builder.rs @@ -1,9 +1,12 @@ -use anyhow::anyhow; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +//#![forbid(unused_variables)] + use chrono::{DateTime, Utc}; use config::shared_constants::DEFAULT_MYSQL_QUERY_RESULT_PAGE_SIZE; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; -use log::{warn, info}; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; #[derive(Serialize)] @@ -35,7 +38,7 @@ pub struct TtsInferenceRecordForList { pub file_size_bytes: u32, pub duration_millis: u32, - pub visibility: RecordVisibility, + pub visibility: Visibility, //pub model_is_mod_approved: bool, // converted //pub maybe_mod_user_token: Option, @@ -115,10 +118,10 @@ impl ListTtsResultsQueryBuilder { let internal_results = self.perform_internal_query(mysql_pool).await?; - let mut first_id = internal_results.first() + let first_id = internal_results.first() .map(|raw_result| raw_result.tts_result_id); - let mut last_id = internal_results.last() + let last_id = internal_results.last() .map(|raw_result| raw_result.tts_result_id); let inference_results = internal_results @@ -135,7 +138,7 @@ impl ListTtsResultsQueryBuilder { maybe_creator_result_id: r.maybe_creator_result_id.map(|v| v as u64).clone(), file_size_bytes: if r.file_size_bytes > 0 { r.file_size_bytes as u32 } else { 0 }, duration_millis: if r.duration_millis > 0 { r.duration_millis as u32 } else { 0 }, - visibility: RecordVisibility::from_str(&r.creator_set_visibility).unwrap_or(RecordVisibility::Public), + visibility: Visibility::from_str(&r.creator_set_visibility).unwrap_or(Visibility::Public), created_at: r.created_at, updated_at: r.updated_at, } diff --git a/crates/lib/database_queries/src/queries/tts/tts_results/query_tts_result.rs b/crates/lib/database_queries/src/queries/tts/tts_results/query_tts_result.rs index c9a6a84653..cf72c53577 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_results/query_tts_result.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_results/query_tts_result.rs @@ -1,15 +1,16 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::column_types::vocoder_type::VocoderType; use crate::helpers::boolean_converters::{nullable_i8_to_bool, i8_to_bool}; -use log::{info, warn, log}; +use enums::core::visibility::Visibility; +use log::warn; use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; -use std::sync::Arc; #[derive(Serialize)] pub struct TtsResultRecordForResponse { @@ -34,7 +35,7 @@ pub struct TtsResultRecordForResponse { pub public_bucket_wav_audio_path: String, pub public_bucket_spectrogram_path: String, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, // Worker hostname that generated the audio. Has the value "unknown" if null. pub generated_by_worker: String, @@ -170,8 +171,8 @@ pub async fn select_tts_result_by_token( public_bucket_spectrogram_path: ir.public_bucket_spectrogram_path, // NB: Fail open/public since we're already looking at it - creator_set_visibility: RecordVisibility::from_str(&ir.creator_set_visibility) - .unwrap_or(RecordVisibility::Public), + creator_set_visibility: Visibility::from_str(&ir.creator_set_visibility) + .unwrap_or(Visibility::Public), generated_by_worker: ir.generated_by_worker.unwrap_or("unknown".to_string()), diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs b/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs index 8017409f17..6079245b6a 100644 --- a/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs +++ b/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs @@ -4,12 +4,11 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::helpers::boolean_converters::{nullable_i8_to_optional_bool, i8_to_bool, nullable_i8_to_bool_default_false}; +use enums::core::visibility::Visibility; use log::warn; use sqlx::MySql; use sqlx::pool::PoolConnection; -use enums::core::visibility::Visibility; use tokens::users::user::UserToken; pub struct SessionUserRecord { @@ -30,8 +29,8 @@ pub struct SessionUserRecord { pub disable_gravatar: bool, pub auto_play_audio_preference: Option, - pub preferred_tts_result_visibility: RecordVisibility, - pub preferred_w2l_result_visibility: RecordVisibility, + pub preferred_tts_result_visibility: Visibility, + pub preferred_w2l_result_visibility: Visibility, pub auto_play_video_preference: Option, // ===== ROLE ===== // @@ -72,24 +71,6 @@ impl SessionUserRecord { pub fn get_strongly_typed_user_token(&self) -> UserToken { UserToken::new_from_str(&self.user_token) } - - // TODO(bt, 2022-12-20): Expunge RecordVisibility and EntityVisibility from the codebase. - pub fn get_preferred_tts_result_visibility(&self) -> Visibility { - match self.preferred_tts_result_visibility { - RecordVisibility::Public => Visibility::Public, - RecordVisibility::Hidden => Visibility::Hidden, - RecordVisibility::Private => Visibility::Private, - } - } - - // TODO(bt, 2022-12-20): Expunge RecordVisibility and EntityVisibility from the codebase. - pub fn get_preferred_w2l_result_visibility(&self) -> Visibility { - match self.preferred_w2l_result_visibility { - RecordVisibility::Public => Visibility::Public, - RecordVisibility::Hidden => Visibility::Hidden, - RecordVisibility::Private => Visibility::Private, - } - } } pub async fn get_user_session_by_token( @@ -116,8 +97,8 @@ SELECT users.disable_gravatar, users.auto_play_audio_preference, users.auto_play_video_preference, - users.preferred_tts_result_visibility as `preferred_tts_result_visibility: crate::column_types::record_visibility::RecordVisibility`, - users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: crate::column_types::record_visibility::RecordVisibility`, + users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::core::visibility::Visibility`, + users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::core::visibility::Visibility`, users.user_role_slug, users.is_banned, @@ -238,8 +219,8 @@ struct SessionUserRawDbRecord { disable_gravatar: i8, auto_play_audio_preference: Option, auto_play_video_preference: Option, - preferred_tts_result_visibility: RecordVisibility, - preferred_w2l_result_visibility: RecordVisibility, + preferred_tts_result_visibility: Visibility, + preferred_w2l_result_visibility: Visibility, user_role_slug: String, is_banned: i8, diff --git a/crates/lib/database_queries/src/queries/w2l/w2l_inference_jobs/w2l_inference_job_queries.rs b/crates/lib/database_queries/src/queries/w2l/w2l_inference_jobs/w2l_inference_job_queries.rs index 61ac44ba89..9a96aed0a2 100644 --- a/crates/lib/database_queries/src/queries/w2l/w2l_inference_jobs/w2l_inference_job_queries.rs +++ b/crates/lib/database_queries/src/queries/w2l/w2l_inference_jobs/w2l_inference_job_queries.rs @@ -5,8 +5,8 @@ use anyhow::anyhow; use chrono::{Utc, DateTime}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::tokens::Tokens; +use enums::core::visibility::Visibility; use log::{warn, info}; use sqlx::MySqlPool; use std::path::Path; @@ -38,7 +38,7 @@ pub struct W2lInferenceJobRecord { pub creator_ip_address: String, pub maybe_creator_user_token: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub disable_end_bump: i8, // bool pub disable_watermark: i8, // bool @@ -76,7 +76,7 @@ SELECT creator_ip_address, maybe_creator_user_token, - creator_set_visibility as `creator_set_visibility: crate::column_types::record_visibility::RecordVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, disable_end_bump, disable_watermark, diff --git a/crates/lib/database_queries/src/queries/w2l/w2l_results/list_w2l_inference_results_query_builder.rs b/crates/lib/database_queries/src/queries/w2l/w2l_results/list_w2l_inference_results_query_builder.rs index 6c4a6f1760..aa3d299a28 100644 --- a/crates/lib/database_queries/src/queries/w2l/w2l_results/list_w2l_inference_results_query_builder.rs +++ b/crates/lib/database_queries/src/queries/w2l/w2l_results/list_w2l_inference_results_query_builder.rs @@ -1,9 +1,12 @@ -use anyhow::anyhow; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use chrono::{DateTime, Utc}; use config::shared_constants::DEFAULT_MYSQL_QUERY_RESULT_PAGE_SIZE; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; -use log::{warn, info}; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; #[derive(Serialize)] @@ -38,7 +41,7 @@ pub struct W2lInferenceRecordForList { pub frame_height: u32, pub duration_millis: u32, - pub visibility: RecordVisibility, + pub visibility: Visibility, //pub template_is_mod_approved: bool, // converted //pub maybe_mod_user_token: Option, @@ -132,10 +135,10 @@ impl ListW2lResultsQueryBuilder { let internal_results = self.perform_internal_query(mysql_pool).await?; - let mut first_id = internal_results.first() + let first_id = internal_results.first() .map(|raw_result| raw_result.w2l_result_id); - let mut last_id = internal_results.last() + let last_id = internal_results.last() .map(|raw_result| raw_result.w2l_result_id); let inference_results = internal_results @@ -155,7 +158,7 @@ impl ListW2lResultsQueryBuilder { frame_width: if r.frame_width > 0 { r.frame_width as u32 } else { 0 }, frame_height: if r.frame_height > 0 { r.frame_height as u32 } else { 0 }, duration_millis: if r.duration_millis > 0 { r.duration_millis as u32 } else { 0 }, - visibility: RecordVisibility::from_str(&r.creator_set_visibility).unwrap_or(RecordVisibility::Public), + visibility: Visibility::from_str(&r.creator_set_visibility).unwrap_or(Visibility::Public), created_at: r.created_at, updated_at: r.updated_at, } @@ -255,7 +258,7 @@ LEFT OUTER JOIN users let mut query = "".to_string(); - if let Some(offset) = self.offset { + if let Some(_offset) = self.offset { if !first_predicate_added { query.push_str(" WHERE"); first_predicate_added = true; @@ -282,7 +285,7 @@ LEFT OUTER JOIN users } } - if let Some(username) = self.scope_creator_username.as_deref() { + if let Some(_username) = self.scope_creator_username.as_deref() { if !first_predicate_added { query.push_str(" WHERE users.username = ?"); first_predicate_added = true; diff --git a/crates/lib/database_queries/src/queries/w2l/w2l_results/query_w2l_result.rs b/crates/lib/database_queries/src/queries/w2l/w2l_results/query_w2l_result.rs index 2d8339d6ce..a0302b11c0 100644 --- a/crates/lib/database_queries/src/queries/w2l/w2l_results/query_w2l_result.rs +++ b/crates/lib/database_queries/src/queries/w2l/w2l_results/query_w2l_result.rs @@ -1,13 +1,15 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::helpers::boolean_converters::nullable_i8_to_bool; -use log::{info, warn, log}; +use enums::core::visibility::Visibility; +use log::warn; use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; #[derive(Serialize)] pub struct W2lResultRecordForResponse { @@ -30,7 +32,7 @@ pub struct W2lResultRecordForResponse { pub maybe_template_creator_display_name: Option, pub maybe_template_creator_gravatar_hash: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub file_size_bytes: u32, pub frame_width: u32, @@ -148,8 +150,8 @@ pub async fn select_w2l_result_by_token( maybe_template_creator_gravatar_hash: ir.maybe_template_creator_gravatar_hash, // NB: Fail open/public since we're already looking at it - creator_set_visibility: RecordVisibility::from_str(&ir.creator_set_visibility) - .unwrap_or(RecordVisibility::Public), + creator_set_visibility: Visibility::from_str(&ir.creator_set_visibility) + .unwrap_or(Visibility::Public), //template_is_mod_approved: if ir.template_is_mod_approved == 0 { false } else { true }, diff --git a/crates/lib/database_queries/src/queries/w2l/w2l_templates/get_w2l_template.rs b/crates/lib/database_queries/src/queries/w2l/w2l_templates/get_w2l_template.rs index 3c9df4abb3..f50f3f97f7 100644 --- a/crates/lib/database_queries/src/queries/w2l/w2l_templates/get_w2l_template.rs +++ b/crates/lib/database_queries/src/queries/w2l/w2l_templates/get_w2l_template.rs @@ -1,14 +1,15 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::helpers::boolean_converters::{nullable_i8_to_optional_bool, i8_to_bool}; -use log::{info, warn, log}; +use enums::core::visibility::Visibility; +use log::warn; use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; -use std::sync::Arc; // FIXME: This is the old style of query scoping and shouldn't be copied. // The moderator-only fields are good practice, though. @@ -29,7 +30,7 @@ pub struct W2lTemplateRecordForResponse { pub duration_millis: u32, pub maybe_image_object_name: Option, pub maybe_video_object_name: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_public_listing_approved: Option, pub is_locked_from_use: bool, pub is_locked_from_user_modification: bool, @@ -122,8 +123,8 @@ pub async fn select_w2l_template_by_token( maybe_image_object_name: template.maybe_public_bucket_preview_image_object_name, maybe_video_object_name: template.maybe_public_bucket_preview_video_object_name, // NB: Fail open/public with creator_set_visibility since we're already looking at it - creator_set_visibility: RecordVisibility::from_str(&template.creator_set_visibility) - .unwrap_or(RecordVisibility::Public), + creator_set_visibility: Visibility::from_str(&template.creator_set_visibility) + .unwrap_or(Visibility::Public), is_public_listing_approved: nullable_i8_to_optional_bool(template.is_public_listing_approved), is_locked_from_use: i8_to_bool(template.is_locked_from_use), is_locked_from_user_modification: i8_to_bool(template.is_locked_from_user_modification), diff --git a/crates/plugins/users/src/endpoints/edit_profile_handler.rs b/crates/plugins/users/src/endpoints/edit_profile_handler.rs index 1fdf818383..9850dfefd4 100644 --- a/crates/plugins/users/src/endpoints/edit_profile_handler.rs +++ b/crates/plugins/users/src/endpoints/edit_profile_handler.rs @@ -14,11 +14,11 @@ use crate::validations::validate_profile_github_username::validate_profile_githu use crate::validations::validate_profile_twitch_username::validate_profile_twitch_username; use crate::validations::validate_profile_twitter_username::{normalize_twitter_username_for_storage, validate_profile_twitter_username}; use crate::validations::validate_profile_website_url::validate_profile_website_url; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::users::user_profiles::edit_user_profile_as_account_holder::edit_user_profile_as_account_holder; use database_queries::queries::users::user_profiles::edit_user_profile_as_mod::edit_user_profile_as_mod; use database_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username; use database_queries::queries::users::user_profiles::{edit_user_profile_as_account_holder, edit_user_profile_as_mod}; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::warn; @@ -47,8 +47,8 @@ pub struct EditProfileRequest { pub cashapp_username: Option, pub website_url: Option, - pub preferred_tts_result_visibility: Option, - pub preferred_w2l_result_visibility: Option, + pub preferred_tts_result_visibility: Option, + pub preferred_w2l_result_visibility: Option, } #[derive(Serialize)] @@ -242,10 +242,10 @@ pub async fn edit_profile_handler( let ip_address = get_request_ip(&http_request); let preferred_tts_result_visibility = request.preferred_tts_result_visibility - .unwrap_or(RecordVisibility::Hidden); + .unwrap_or(Visibility::Hidden); let preferred_w2l_result_visibility = request.preferred_w2l_result_visibility - .unwrap_or(RecordVisibility::Hidden); + .unwrap_or(Visibility::Hidden); let query_result = if editor_is_original_user { edit_user_profile_as_account_holder( diff --git a/crates/plugins/users/src/utils/user_session_extended.rs b/crates/plugins/users/src/utils/user_session_extended.rs index 821d416d96..c784484e6b 100644 --- a/crates/plugins/users/src/utils/user_session_extended.rs +++ b/crates/plugins/users/src/utils/user_session_extended.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use database_queries::column_types::record_visibility::RecordVisibility; +use enums::core::visibility::Visibility; #[derive(Clone, Default)] pub struct UserSessionExtended { @@ -54,8 +54,8 @@ impl Default for UserSessionSubscriptionPlan { pub struct UserSessionPreferences { pub disable_gravatar: bool, pub auto_play_audio_preference: Option, - pub preferred_tts_result_visibility: RecordVisibility, - pub preferred_w2l_result_visibility: RecordVisibility, + pub preferred_tts_result_visibility: Visibility, + pub preferred_w2l_result_visibility: Visibility, pub auto_play_video_preference: Option, } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index 78c640f35d..4043bfdf9c 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -12,7 +12,6 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::media_uploads::insert_media_upload::{Args, insert_media_upload}; use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; @@ -123,7 +122,7 @@ pub async fn upload_audio_handler( let creator_set_visibility = maybe_user_session .as_ref() - .map(|user_session| user_session.get_preferred_tts_result_visibility()) // TODO: We need a new type of visibility control. + .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); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs index 98118e7c4d..80b3b4342e 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs @@ -11,11 +11,11 @@ use container_common::i18n::supported_languages_for_models::get_canonicalized_la use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::column_types::vocoder_type::VocoderType; use database_queries::queries::tts::tts_models::edit_tts_model_details::{edit_tts_model_details_as_author, edit_tts_model_details_as_mod}; use database_queries::queries::tts::tts_models::edit_tts_model_moderator_details::edit_tts_model_moderator_details; use database_queries::queries::tts::tts_models::get_tts_model::get_tts_model_by_token; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use language_tags::LanguageTag; use log::{info, warn, error}; @@ -193,7 +193,7 @@ pub async fn edit_tts_model_handler( let mut ietf_language_tag = None; let mut ietf_primary_language_subtag = None; - let mut creator_set_visibility = RecordVisibility::Public; + let mut creator_set_visibility = Visibility::Public; let mut maybe_default_pretrained_vocoder = model_record.maybe_default_pretrained_vocoder .clone(); @@ -248,7 +248,7 @@ pub async fn edit_tts_model_handler( .unwrap_or(DEFAULT_IETF_PRIMARY_LANGUAGE_SUBTAG.to_string()); if let Some(visibility) = request.creator_set_visibility.as_deref() { - creator_set_visibility = RecordVisibility::from_str(visibility) + creator_set_visibility = Visibility::from_str(visibility) .map_err(|_| EditTtsModelError::BadInput("bad record visibility".to_string()))?; } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs index 9a64bc755c..2375c4f890 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs @@ -1,23 +1,19 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::web::{Path, Json}; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::web::Path; +use actix_web::{web, HttpResponse, HttpRequest}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::tts::tts_results::query_tts_result::select_tts_result_by_token; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; +use log::warn; use std::fmt; use std::sync::Arc; @@ -107,7 +103,7 @@ pub async fn edit_tts_inference_result_handler( &server_state.mysql_pool ).await; - let mut inference_result = match inference_result_query_result { + let inference_result = match inference_result_query_result { Err(e) => { warn!("query error: {:?}", e); return Err(EditTtsResultError::ServerError); @@ -129,10 +125,10 @@ pub async fn edit_tts_inference_result_handler( // Author + Mod fields. // These fields must be present on all requests. - let mut creator_set_visibility = RecordVisibility::Public; + let mut creator_set_visibility = Visibility::Public; if let Some(visibility) = request.creator_set_visibility.as_deref() { - creator_set_visibility = RecordVisibility::from_str(visibility) + creator_set_visibility = Visibility::from_str(visibility) .map_err(|_| EditTtsResultError::BadInput("bad record visibility".to_string()))?; } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs index 53a4523622..3eb125ee8e 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs @@ -1,29 +1,28 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::{web, HttpResponse, HttpRequest}; +use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session; use crate::http_server::endpoints::investor_demo::demo_cookie::request_has_demo_cookie; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use crate::validations::model_uploads::validate_model_title; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::tts::tts_inference_jobs::insert_tts_inference_job::TtsInferenceJobInsertBuilder; use database_queries::tokens::Tokens; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_api_token::get_request_api_token; use http_server_common::request::get_request_header_optional::get_request_header_optional; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; +use log::{info, warn}; use r2d2_redis::redis::Commands; use redis_common::redis_keys::RedisKeys; -use regex::Regex; use std::fmt; use std::sync::Arc; -use tts_common::priority::{FAKEYOU_LOGGED_IN_PRIORITY_LEVEL, FAKEYOU_ANONYMOUS_PRIORITY_LEVEL, FAKEYOU_INVESTOR_PRIORITY_LEVEL, FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL}; +use tts_common::priority::{FAKEYOU_INVESTOR_PRIORITY_LEVEL, FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL}; use user_input_common::check_for_slurs::contains_slurs; -use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session; // TODO: Temporary for investor demo const STORYTELLER_DEMO_COOKIE_NAME : &'static str = "storyteller_demo"; @@ -37,7 +36,7 @@ pub struct InferTtsRequest { uuid_idempotency_token: String, tts_model_token: String, inference_text: String, - creator_set_visibility: Option, + creator_set_visibility: Option, is_storyteller_demo: Option, } @@ -238,17 +237,17 @@ pub async fn infer_tts_handler( let ip_address = get_request_ip(&http_request); - let maybe_user_preferred_visibility : Option = maybe_user_session + let maybe_user_preferred_visibility : Option = maybe_user_session .as_ref() .map(|user_session| user_session.preferences.preferred_tts_result_visibility); let set_visibility = request.creator_set_visibility .or(maybe_user_preferred_visibility) - .unwrap_or(RecordVisibility::Public); + .unwrap_or(Visibility::Public); // This token is returned to the client. let job_token = Tokens::new_tts_inference_job() - .map_err(|e| { + .map_err(|_e| { warn!("Error creating token"); InferTtsError::ServerError })?; @@ -291,7 +290,7 @@ pub async fn infer_tts_handler( }; let body = serde_json::to_string(&response) - .map_err(|e| InferTtsError::ServerError)?; + .map_err(|_e| InferTtsError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs index 89019199df..84d491bd81 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs @@ -9,9 +9,9 @@ use actix_web::web::Path; use actix_web::{web, HttpResponse, HttpRequest}; use chrono::{DateTime, Utc}; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::column_types::vocoder_type::VocoderType; use database_queries::queries::tts::tts_models::get_tts_model::get_tts_model_by_token_using_connection; +use enums::core::visibility::Visibility; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::warn; use std::fmt; @@ -80,7 +80,7 @@ pub struct TtsModelInfo { pub maybe_suggested_unique_bot_command: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_locked_from_use: bool, pub is_locked_from_user_modification: bool, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs index fe63144ee4..78f1ccb5b6 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs @@ -1,23 +1,19 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::web::{Path, Json}; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::web::Path; +use actix_web::{web, HttpResponse, HttpRequest}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::w2l::w2l_results::query_w2l_result::select_w2l_result_by_token; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; +use log::warn; use std::fmt; use std::sync::Arc; @@ -107,7 +103,7 @@ pub async fn edit_w2l_inference_result_handler( &server_state.mysql_pool ).await; - let mut inference_result = match inference_result_query_result { + let inference_result = match inference_result_query_result { Err(e) => { warn!("query error: {:?}", e); return Err(EditW2lResultError::ServerError); @@ -129,10 +125,10 @@ pub async fn edit_w2l_inference_result_handler( // Author + Mod fields. // These fields must be present on all requests. - let mut creator_set_visibility = RecordVisibility::Public; + let mut creator_set_visibility = Visibility::Public; if let Some(visibility) = request.creator_set_visibility.as_deref() { - creator_set_visibility = RecordVisibility::from_str(visibility) + creator_set_visibility = Visibility::from_str(visibility) .map_err(|_| EditW2lResultError::BadInput("bad record visibility".to_string()))?; } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs index e60c7172ba..714b8c80a9 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs @@ -10,8 +10,8 @@ use actix_web::{web, HttpResponse, HttpRequest}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::w2l::w2l_templates::get_w2l_template::select_w2l_template_by_token; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use log::{info, warn}; use sqlx::MySqlPool; @@ -145,7 +145,7 @@ pub async fn edit_w2l_template_handler( let mut title = None; let mut description_markdown = None; let mut description_html = None; - let mut creator_set_visibility = RecordVisibility::Public; + let mut creator_set_visibility = Visibility::Public; if let Some(payload) = request.title.as_deref() { if contains_slurs(payload) { @@ -168,7 +168,7 @@ pub async fn edit_w2l_template_handler( } if let Some(visibility) = request.creator_set_visibility.as_deref() { - creator_set_visibility = RecordVisibility::from_str(visibility) + creator_set_visibility = Visibility::from_str(visibility) .map_err(|_| EditW2lTemplateError::BadInput("bad record visibility".to_string()))?; } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs index d0671c948b..3d58c2bd0f 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs @@ -1,22 +1,19 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::{web, HttpResponse, HttpRequest}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use crate::validations::model_uploads::validate_model_title; -use database_queries::column_types::record_visibility::RecordVisibility; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; +use log::{info, warn}; use r2d2_redis::redis::Commands; use redis_common::redis_keys::RedisKeys; -use regex::Regex; -use sqlx::error::DatabaseError; use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; use std::fmt; use std::sync::Arc; @@ -24,7 +21,7 @@ use std::sync::Arc; pub struct InferW2lRequest { w2l_template_token: Option, tts_inference_result_token: Option, - creator_set_visibility: Option, + creator_set_visibility: Option, } #[derive(Serialize)] @@ -93,7 +90,7 @@ pub async fn infer_w2l_handler( // return Err(InferW2lError::RateLimited); //} - let mut maybe_user_token : Option = maybe_session + let maybe_user_token : Option = maybe_session .as_ref() .map(|user_session| user_session.user_token.to_string()); @@ -176,7 +173,7 @@ SET // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) match err { Database(err) => { - let maybe_code = err.code().map(|c| c.into_owned()); + let _maybe_code = err.code().map(|c| c.into_owned()); /*match maybe_code.as_deref() { Some("23000") => { if err.message().contains("username") { @@ -201,7 +198,7 @@ SET }; let body = serde_json::to_string(&response) - .map_err(|e| InferW2lError::ServerError)?; + .map_err(|_e| InferW2lError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs index dc4f8eb91a..df0e8b1166 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs @@ -1,23 +1,22 @@ -use actix_http::http::header; -use actix_multipart::{Multipart, Field}; -use actix_web::HttpResponseBuilder; -use actix_web::body::ResponseBody::Body; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use actix_multipart::Multipart; use actix_web::http::StatusCode; -use actix_web::http::header::ContentDisposition; -use actix_web::web::{Data, Json, BytesMut}; -use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer, HttpRequest, Either, ResponseError}; +use actix_web::web::BytesMut; +use actix_web::{web, HttpResponse, HttpRequest, ResponseError}; use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; use container_common::token::random_uuid::generate_random_uuid; use crate::http_server::web_utils::read_multipart_field_bytes::checked_read_multipart_bytes; -use crate::http_server::web_utils::read_multipart_field_bytes::read_multipart_field_as_boolean; use crate::http_server::web_utils::read_multipart_field_bytes::read_multipart_field_as_text; -use crate::http_server::web_utils::read_multipart_field_bytes::read_multipart_field_bytes; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::tokens::Tokens; -use futures::{StreamExt, TryStreamExt}; +use enums::core::visibility::Visibility; +use futures::TryStreamExt; use http_server_common::request::get_request_ip::get_request_ip; use log::{warn, info}; use r2d2_redis::redis::Commands; @@ -25,7 +24,6 @@ use redis_common::redis_keys::RedisKeys; use sqlx::MySqlPool; use sqlx::error::Error::Database; use std::fmt; -use std::io::Write; use std::sync::Arc; use storage_buckets_common::bucket_paths::hash_to_bucket_path; @@ -127,16 +125,16 @@ pub async fn enqueue_infer_w2l_with_uploads( // ==================== SESSION DETAILS ==================== // - let mut maybe_user_token : Option = maybe_session + let maybe_user_token : Option = maybe_session .as_ref() .map(|user_session| user_session.user_token.to_string()); - let maybe_user_preferred_visibility : Option = maybe_session + let maybe_user_preferred_visibility : Option = maybe_session .as_ref() .map(|user_session| user_session.preferred_tts_result_visibility); let set_visibility = maybe_user_preferred_visibility - .unwrap_or(RecordVisibility::Public); + .unwrap_or(Visibility::Public); info!("Enqueue infer w2l by user token: {:?}", maybe_user_token); @@ -279,7 +277,7 @@ pub async fn enqueue_infer_w2l_with_uploads( // This token is returned to the client. let job_token = Tokens::new_w2l_inference_job() - .map_err(|e| { + .map_err(|_e| { warn!("Error creating token"); InferW2lWithUploadError::ServerError })?; @@ -320,7 +318,7 @@ SET .execute(&server_state.mysql_pool) .await; - let record_id = match query_result { + let _record_id = match query_result { Ok(res) => { res.last_insert_id() }, @@ -331,7 +329,7 @@ SET // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) match err { Database(err) => { - let maybe_code = err.code().map(|c| c.into_owned()); + let _maybe_code = err.code().map(|c| c.into_owned()); /*match maybe_code.as_deref() { Some("23000") => { if err.message().contains("username") { @@ -363,7 +361,7 @@ SET }; let body = serde_json::to_string(&response) - .map_err(|e| InferW2lWithUploadError::ServerError)?; + .map_err(|_e| InferW2lWithUploadError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") @@ -391,7 +389,7 @@ WHERE .await; // TODO: This will return error if it doesn't exist let record_exists = match maybe_template { - Ok(record) => { + Ok(_record) => { true }, Err(err) => { From 32c73547986b05b979e3eae6b8597a5c83303cd7 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 20 Dec 2022 03:43:58 -0500 Subject: [PATCH 23/54] Get rid of super legacy CreatorSetVisibility --- .../endpoints/tts/enqueue_upload_tts_model.rs | 36 +++++++------------ .../w2l/enqueue_upload_w2l_template.rs | 36 +++++++------------ 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs index ff8fcf6ba0..400e324316 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs @@ -1,21 +1,20 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::{web, HttpResponse, HttpRequest}; use config::bad_urls::is_bad_tts_model_download_url; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; use database_queries::tokens::Tokens; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::error::DatabaseError; +use log::{info, warn}; use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; use std::fmt; use std::sync::Arc; @@ -31,24 +30,13 @@ pub enum TtsModelType { Talknet, } -#[deprecated(note = "Use `RecordVisibility` instead!")] -#[derive(Deserialize)] -pub enum CreatorSetVisibility { - /// public - Public, - /// hidden - Hidden, - /// private - Private, -} - #[derive(Deserialize)] pub struct UploadTtsModelRequest { idempotency_token: String, title: String, download_url: String, tts_model_type: Option, - creator_set_visibility: Option, + creator_set_visibility: Option, } #[derive(Serialize)] @@ -151,7 +139,7 @@ pub async fn upload_tts_model_handler( // This token is returned to the client. let job_token = Tokens::new_tts_model_upload_job() - .map_err(|e| { + .map_err(|_e| { warn!("Error creating token"); UploadTtsModelError::ServerError })?; @@ -193,7 +181,7 @@ SET // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) match err { Database(err) => { - let maybe_code = err.code().map(|c| c.into_owned()); + let _maybe_code = err.code().map(|c| c.into_owned()); /*match maybe_code.as_deref() { Some("23000") => { if err.message().contains("username") { @@ -226,7 +214,7 @@ SET }; let body = serde_json::to_string(&response) - .map_err(|e| UploadTtsModelError::ServerError)?; + .map_err(|_e| UploadTtsModelError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs index a516a857f0..4528ae381e 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs @@ -1,21 +1,20 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::{web, HttpResponse, HttpRequest}; use config::is_bad_video_download_url::is_bad_video_download_url; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; use database_queries::tokens::Tokens; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::error::DatabaseError; +use log::{info, warn}; use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; use std::fmt; use std::sync::Arc; use user_input_common::check_for_slurs::contains_slurs; @@ -30,24 +29,13 @@ pub enum W2lTemplateType { Image, } -#[deprecated(note = "Use `RecordVisibility` instead!")] -#[derive(Deserialize)] -pub enum CreatorSetVisibility { - /// public - Public, - /// hidden - Hidden, - /// private - Private, -} - #[derive(Deserialize)] pub struct UploadW2lTemplateRequest { idempotency_token: String, title: String, download_url: String, template_type: Option, - creator_set_visibility: Option, + creator_set_visibility: Option, } #[derive(Serialize)] @@ -147,7 +135,7 @@ pub async fn upload_w2l_template_handler( // This token is returned to the client. let job_token = Tokens::new_w2l_template_upload_job() - .map_err(|e| { + .map_err(|_e| { warn!("Error creating token"); UploadW2lTemplateError::ServerError })?; @@ -190,7 +178,7 @@ SET // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) match err { Database(err) => { - let maybe_code = err.code().map(|c| c.into_owned()); + let _maybe_code = err.code().map(|c| c.into_owned()); /*match maybe_code.as_deref() { Some("23000") => { if err.message().contains("username") { @@ -223,7 +211,7 @@ SET }; let body = serde_json::to_string(&response) - .map_err(|e| UploadW2lTemplateError::ServerError)?; + .map_err(|_e| UploadW2lTemplateError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") From a1510d45b2c53162f945da94a45c92d4be5b2389 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 20 Dec 2022 04:37:59 -0500 Subject: [PATCH 24/54] Media uploads "working" from frontend. Need to handle file + upload --- .../media_uploads/insert_media_upload.rs | 42 +++++++++++++------ .../media_uploads/drain_multipart_request.rs | 2 +- crates/types/tokens/src/files/media_upload.rs | 2 +- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs index 5997a291ca..1206b5b5e1 100644 --- a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs +++ b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs @@ -37,14 +37,7 @@ pub struct Args <'a> { pub async fn insert_media_upload(args: Args<'_>) -> AnyhowResult { - let query = sqlx::query!( - r#" -INSERT INTO media_uploads -SET - token = ?, - uuid_idempotency_token = ?, - - media_type = ?, + /* maybe_original_filename = ?, original_file_size_bytes = ?, original_duration_millis = ?, @@ -53,6 +46,31 @@ SET maybe_original_video_encoding = ?, maybe_original_frame_width = ?, maybe_original_frame_height = ?, + + + + INSERT INTO + media_uploads + SET + token = 'foo', + uuid_idempotency_token = 'bar', + public_bucket_directory_full_path = 'asdf', + extra_file_modification_info = 'asdf', + maybe_creator_user_token = 'asdf', + maybe_creator_anonymous_visitor_token = 'asdf', + creator_ip_address = 'asdf', + creator_set_visibility = 'asdf', + maybe_creator_synthetic_id = 'asdf' + + */ + let query = sqlx::query!( + r#" +INSERT INTO media_uploads +SET + token = ?, + uuid_idempotency_token = ?, + + media_type = ?, checksum_sha2 = ?, public_bucket_directory_full_path = ?, @@ -68,15 +86,15 @@ SET args.token, args.uuid_idempotency_token, - args.media_type, - args.maybe_original_filename, + "audio", // TODO: args.media_type, + /*args.maybe_original_filename, args.original_file_size_bytes, args.original_duration_millis, args.maybe_original_mime_type, args.maybe_original_audio_encoding, args.maybe_original_video_encoding, args.maybe_original_frame_width, - args.maybe_original_frame_height, + args.maybe_original_frame_height,*/ args.checksum_sha2, args.public_bucket_directory_full_path, @@ -85,7 +103,7 @@ SET args.maybe_creator_user_token, args.maybe_creator_anonymous_visitor_token, args.creator_ip_address, - args.creator_set_visibility, + args.creator_set_visibility.to_str(), args.maybe_creator_synthetic_id, ); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs index c9d90a31ae..b6a30f44cd 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs @@ -37,7 +37,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow e })?; }, - Some("file_bytes") => { + Some("file") => { file_name = field_filename.clone(); file_bytes = checked_read_multipart_bytes(&mut field).await .map_err(|e| { diff --git a/crates/types/tokens/src/files/media_upload.rs b/crates/types/tokens/src/files/media_upload.rs index 542983a739..df2490d560 100644 --- a/crates/types/tokens/src/files/media_upload.rs +++ b/crates/types/tokens/src/files/media_upload.rs @@ -9,4 +9,4 @@ use std::fmt::Debug; pub struct MediaUploadToken(String); impl_string_token!(MediaUploadToken); -impl_crockford_generator!(MediaUploadToken, 32usize, EntityType::DownloadJob, CrockfordLower); +impl_crockford_generator!(MediaUploadToken, 32usize, EntityType::MediaUpload, CrockfordLower); From 43c890b27c9dbc779de59fcb4df93170b7be7841 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 20 Dec 2022 21:52:43 -0500 Subject: [PATCH 25/54] Hack for sqlx to insert enums into varchar, but case still wrong --- .../media_uploads/insert_media_upload.rs | 4 +- .../endpoints/media_uploads/upload_audio.rs | 2 +- crates/types/enums/src/core/visibility.rs | 43 +++++++++ .../enums/src/files/media_upload_type.rs | 93 +++++++++++++++++++ crates/types/enums/src/files/mod.rs | 1 + crates/types/enums/src/lib.rs | 4 + crates/types/enums/src/macros.rs | 30 ++++++ .../src/db/enums/media_upload_type.rs | 53 ----------- .../types/reusable_types/src/db/enums/mod.rs | 1 - 9 files changed, 174 insertions(+), 57 deletions(-) create mode 100644 crates/types/enums/src/files/media_upload_type.rs create mode 100644 crates/types/enums/src/files/mod.rs create mode 100644 crates/types/enums/src/macros.rs delete mode 100644 crates/types/reusable_types/src/db/enums/media_upload_type.rs diff --git a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs index 1206b5b5e1..3fbd1ba57c 100644 --- a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs +++ b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; use enums::core::visibility::Visibility; -use reusable_types::db::enums::media_upload_type::MediaUploadType; +use enums::files::media_upload_type::MediaUploadType; use reusable_types::db::payloads::MediaUploadDetails; use sqlx::MySqlPool; use tokens::files::media_upload::MediaUploadToken; @@ -86,7 +86,7 @@ SET args.token, args.uuid_idempotency_token, - "audio", // TODO: args.media_type, + args.media_type, /*args.maybe_original_filename, args.original_file_size_bytes, args.original_duration_millis, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index 4043bfdf9c..47d2ac18f2 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -19,7 +19,7 @@ use http_server_common::response::serialize_as_json_error::serialize_as_json_err use log::{info, warn, log}; use regex::Regex; use reusable_types::db::enums::generic_download_type::GenericDownloadType; -use reusable_types::db::enums::media_upload_type::MediaUploadType; +use enums::files::media_upload_type::MediaUploadType; use reusable_types::db::payloads::MediaUploadDetails; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; diff --git a/crates/types/enums/src/core/visibility.rs b/crates/types/enums/src/core/visibility.rs index 6689fe9b94..2160927f27 100644 --- a/crates/types/enums/src/core/visibility.rs +++ b/crates/types/enums/src/core/visibility.rs @@ -30,6 +30,33 @@ pub enum Visibility { // have not yet been migrated to this scheme. } + +impl_string_enum!(Visibility); + +// For reference, here's what the serde implementation might be if manually written. +// This may be useful for designing composite types in the future: +// +// use serde::{Deserializer, Serializer}; +// +// impl serde::Serialize for UserToken { +// fn serialize(&self, serializer: S) -> Result { +// serializer.serialize_str(&self.0) +// } +// } +// +// impl<'de> serde::Deserialize<'de> for UserToken { +// fn deserialize>(d: D) -> Result { +// let s = String::deserialize(d)?; +// Ok(UserToken(s)) +// } +// } + +//impl sqlx::Type for Visibility { +// fn type_info() -> sqlx_core::database::TypeInfo { +// todo!() +// } +//} + impl Default for Visibility { fn default() -> Self { Self::Public } } @@ -87,6 +114,22 @@ mod tests { assert!(Visibility::from_str("foo").is_err()); } + mod traits { + use crate::core::visibility::Visibility; + + #[test] + fn display() { + let visibility = Visibility::Hidden; + assert_eq!(format!("{}", visibility), "hidden".to_string()); + } + + #[test] + fn debug() { + let visibility = Visibility::Private; + assert_eq!(format!("{:?}", visibility), "private".to_string()); + } + } + #[derive(Serialize, Deserialize, PartialEq, Debug)] struct CompositeType { visibility: Visibility, diff --git a/crates/types/enums/src/files/media_upload_type.rs b/crates/types/enums/src/files/media_upload_type.rs new file mode 100644 index 0000000000..93157a3771 --- /dev/null +++ b/crates/types/enums/src/files/media_upload_type.rs @@ -0,0 +1,93 @@ +use sqlx::Type; +use serde::Deserialize; +use serde::Serialize; +use sqlx_core::database::HasArguments; +use sqlx_core::encode::IsNull; +use sqlx_core::mysql::MySql; + +/// Used in the `media_uploads` table in a `VARCHAR` field. +/// +/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. +//#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +//#[sqlx(rename_all = "lowercase")] +pub enum MediaUploadType { + #[serde(rename = "audio")] + //#[sqlx(rename = "audio")] + Audio, + #[serde(rename = "video")] + //#[sqlx(rename = "video")] + Video, +} + +// Solution adapted from https://github.com/launchbadge/sqlx/discussions/1502 +impl Type for MediaUploadType { + fn type_info() -> sqlx::mysql::MySqlTypeInfo { + String::type_info() + } +} + + +impl<'q> sqlx::Encode<'q, sqlx::MySql> for MediaUploadType { + fn encode_by_ref(&self, buf: &mut >::ArgumentBuffer) -> IsNull { + self.to_string().encode_by_ref(buf) + } +} + +impl<'r> sqlx::Decode<'r, sqlx::MySql> for MediaUploadType { + fn decode( + value: sqlx::mysql::MySqlValueRef<'r>, + ) -> Result { + let string = String::decode(value)?; + let value = MediaUploadType::from_str(&string)?; + //let value = string.parse()?; + Ok(value) + } +} + + +impl_string_enum!(MediaUploadType); + +/// NB: Legacy API for older code. +impl MediaUploadType { + pub fn to_str(&self) -> &'static str { + match self { + Self::Audio => "audio", + Self::Video => "video", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "audio" => Ok(Self::Audio), + "video" => Ok(Self::Video), + _ => Err(format!("invalid value: {:?}", value)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::files::media_upload_type::MediaUploadType; + use crate::test_helpers::assert_serialization; + + #[test] + fn test_serialization() { + assert_serialization(MediaUploadType::Audio, "audio"); + assert_serialization(MediaUploadType::Video, "video"); + } + + #[test] + fn test_to_str() { + assert_eq!(MediaUploadType::Audio.to_str(), "audio"); + assert_eq!(MediaUploadType::Video.to_str(), "video"); + } + + #[test] + fn test_from_str() { + assert_eq!(MediaUploadType::from_str("audio").unwrap(), MediaUploadType::Audio); + assert_eq!(MediaUploadType::from_str("video").unwrap(), MediaUploadType::Video); + assert!(MediaUploadType::from_str("foo").is_err()); + } +} diff --git a/crates/types/enums/src/files/mod.rs b/crates/types/enums/src/files/mod.rs new file mode 100644 index 0000000000..8c653cf6ad --- /dev/null +++ b/crates/types/enums/src/files/mod.rs @@ -0,0 +1 @@ +pub mod media_upload_type; diff --git a/crates/types/enums/src/lib.rs b/crates/types/enums/src/lib.rs index 7f35b53561..f9cd1566e8 100644 --- a/crates/types/enums/src/lib.rs +++ b/crates/types/enums/src/lib.rs @@ -21,6 +21,10 @@ #[macro_use] extern crate serde_derive; +#[macro_use] +mod macros; + #[cfg(test)] pub mod test_helpers; pub mod core; +pub mod files; diff --git a/crates/types/enums/src/macros.rs b/crates/types/enums/src/macros.rs new file mode 100644 index 0000000000..089031a94d --- /dev/null +++ b/crates/types/enums/src/macros.rs @@ -0,0 +1,30 @@ + +// Default methods for string enum types. +macro_rules! impl_string_enum { + ($t:ident) => { +// // Constructors and accessors. +// impl $t { +// #[inline] +// pub fn new(value: String) -> Self { +// $t(value) +// } +// +// #[inline] +// pub fn new_from_str(value: &str) -> Self { +// $t(value.to_string()) +// } +// +// #[inline] +// pub fn as_str(&self) -> &str { +// &self.0 +// } +// } + + // Display trait. + impl std::fmt::Display for $t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } + } + } +} diff --git a/crates/types/reusable_types/src/db/enums/media_upload_type.rs b/crates/types/reusable_types/src/db/enums/media_upload_type.rs deleted file mode 100644 index c16c49e9a1..0000000000 --- a/crates/types/reusable_types/src/db/enums/media_upload_type.rs +++ /dev/null @@ -1,53 +0,0 @@ -/// Used in the `media_uploads` table. -/// -/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] -#[serde(rename_all = "lowercase")] -#[sqlx(rename_all = "lowercase")] -pub enum MediaUploadType { - Audio, - Video, -} - -/// NB: Legacy API for older code. -impl MediaUploadType { - pub fn to_str(&self) -> &'static str { - match self { - Self::Audio => "audio", - Self::Video => "video", - } - } - - pub fn from_str(value: &str) -> Result { - match value { - "audio" => Ok(Self::Audio), - "video" => Ok(Self::Video), - _ => Err(format!("invalid value: {:?}", value)), - } - } -} - -#[cfg(test)] -mod tests { - use crate::db::enums::media_upload_type::MediaUploadType; - use crate::test_helpers::assert_serialization; - - #[test] - fn test_serialization() { - assert_serialization(MediaUploadType::Audio, "audio"); - assert_serialization(MediaUploadType::Video, "video"); - } - - #[test] - fn test_to_str() { - assert_eq!(MediaUploadType::Audio.to_str(), "audio"); - assert_eq!(MediaUploadType::Video.to_str(), "video"); - } - - #[test] - fn test_from_str() { - assert_eq!(MediaUploadType::from_str("audio").unwrap(), MediaUploadType::Audio); - assert_eq!(MediaUploadType::from_str("video").unwrap(), MediaUploadType::Video); - assert!(MediaUploadType::from_str("foo").is_err()); - } -} diff --git a/crates/types/reusable_types/src/db/enums/mod.rs b/crates/types/reusable_types/src/db/enums/mod.rs index 226cd89c3c..21cc5a7616 100644 --- a/crates/types/reusable_types/src/db/enums/mod.rs +++ b/crates/types/reusable_types/src/db/enums/mod.rs @@ -3,5 +3,4 @@ pub mod generic_download_type; pub mod generic_inference_type; -pub mod media_upload_type; pub mod vocoder_type; From 07d13a4ab53b2ad05a6aa875e4a077506bcf109b Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 01:19:29 -0500 Subject: [PATCH 26/54] proper case for enum mysql insert --- .../enums/src/files/media_upload_type.rs | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/crates/types/enums/src/files/media_upload_type.rs b/crates/types/enums/src/files/media_upload_type.rs index 93157a3771..59736ee74d 100644 --- a/crates/types/enums/src/files/media_upload_type.rs +++ b/crates/types/enums/src/files/media_upload_type.rs @@ -1,52 +1,55 @@ -use sqlx::Type; use serde::Deserialize; use serde::Serialize; +use sqlx::Decode; +use sqlx::Encode; +use sqlx::Type; use sqlx_core::database::HasArguments; use sqlx_core::encode::IsNull; -use sqlx_core::mysql::MySql; +use sqlx_core::error::BoxDynError; +use sqlx_core::mysql::{MySql, MySqlTypeInfo, MySqlValueRef}; /// Used in the `media_uploads` table in a `VARCHAR` field. /// /// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. -//#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] #[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] -//#[sqlx(rename_all = "lowercase")] pub enum MediaUploadType { - #[serde(rename = "audio")] - //#[sqlx(rename = "audio")] + /// Audio files: wav, mp3, etc. Audio, - #[serde(rename = "video")] - //#[sqlx(rename = "video")] + /// Video files: mp4, etc. Video, } +// This overt approach is being taken because of the following error: +// `MySqlDatabaseError { code: Some("HY000"), number: 1210, message: "Incorrect arguments to mysqld_stmt_execute" }` +// Basically, sqlx can't turn our enum into a VARCHAR when using #[derive(sqlx::Type)]. +// We further lose the ability to `#[sqlx(rename_all="lowercase")]`, etc., so our encoder/decoder +// need to set the rules. // Solution adapted from https://github.com/launchbadge/sqlx/discussions/1502 -impl Type for MediaUploadType { - fn type_info() -> sqlx::mysql::MySqlTypeInfo { +impl Type for MediaUploadType { + fn type_info() -> MySqlTypeInfo { String::type_info() } } - -impl<'q> sqlx::Encode<'q, sqlx::MySql> for MediaUploadType { +impl<'q> Encode<'q, MySql> for MediaUploadType { fn encode_by_ref(&self, buf: &mut >::ArgumentBuffer) -> IsNull { - self.to_string().encode_by_ref(buf) + // NB: In the absence of `#[derive(sqlx::Type)]` and `#sqlx(rename_all="lowercase")]`, + // this controls the casing of the variants when sent to MySQL. + self.to_str().encode_by_ref(buf) } } -impl<'r> sqlx::Decode<'r, sqlx::MySql> for MediaUploadType { +impl<'r> Decode<'r, MySql> for MediaUploadType { fn decode( - value: sqlx::mysql::MySqlValueRef<'r>, - ) -> Result { + value: MySqlValueRef<'r>, + ) -> Result { let string = String::decode(value)?; let value = MediaUploadType::from_str(&string)?; - //let value = string.parse()?; Ok(value) } } - impl_string_enum!(MediaUploadType); /// NB: Legacy API for older code. From cb1e092ccd4e81b454313d7bde560361b5b901f1 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 01:44:32 -0500 Subject: [PATCH 27/54] tests on derived Display/Debug --- crates/types/enums/src/core/visibility.rs | 10 +++- .../enums/src/files/media_upload_type.rs | 10 +++- crates/types/enums/src/macros.rs | 54 ++++++++++++------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/crates/types/enums/src/core/visibility.rs b/crates/types/enums/src/core/visibility.rs index 2160927f27..7aa2c7e945 100644 --- a/crates/types/enums/src/core/visibility.rs +++ b/crates/types/enums/src/core/visibility.rs @@ -1,3 +1,8 @@ +#[cfg(test)] +use strum::EnumCount; +#[cfg(test)] +use strum::EnumIter; + /// Visibility /// /// Used in various database tables (as enums! careful!) and the HTTP API to convey @@ -11,7 +16,8 @@ /// /// *DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY!* /// -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] +#[cfg_attr(test, derive(EnumIter, EnumCount))] +#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize, sqlx::Type)] #[serde(rename_all = "lowercase")] #[sqlx(rename_all = "lowercase")] pub enum Visibility { @@ -31,7 +37,7 @@ pub enum Visibility { } -impl_string_enum!(Visibility); +impl_enum_display_and_debug_for_to_str!(Visibility); // For reference, here's what the serde implementation might be if manually written. // This may be useful for designing composite types in the future: diff --git a/crates/types/enums/src/files/media_upload_type.rs b/crates/types/enums/src/files/media_upload_type.rs index 59736ee74d..d0b6c66237 100644 --- a/crates/types/enums/src/files/media_upload_type.rs +++ b/crates/types/enums/src/files/media_upload_type.rs @@ -8,10 +8,16 @@ use sqlx_core::encode::IsNull; use sqlx_core::error::BoxDynError; use sqlx_core::mysql::{MySql, MySqlTypeInfo, MySqlValueRef}; +#[cfg(test)] +use strum::EnumCount; +#[cfg(test)] +use strum::EnumIter; + /// Used in the `media_uploads` table in a `VARCHAR` field. /// /// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(EnumIter, EnumCount))] +#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum MediaUploadType { /// Audio files: wav, mp3, etc. @@ -50,7 +56,7 @@ impl<'r> Decode<'r, MySql> for MediaUploadType { } } -impl_string_enum!(MediaUploadType); +impl_enum_display_and_debug_for_to_str!(MediaUploadType); /// NB: Legacy API for older code. impl MediaUploadType { diff --git a/crates/types/enums/src/macros.rs b/crates/types/enums/src/macros.rs index 089031a94d..79f7694e1d 100644 --- a/crates/types/enums/src/macros.rs +++ b/crates/types/enums/src/macros.rs @@ -1,30 +1,44 @@ -// Default methods for string enum types. -macro_rules! impl_string_enum { +/// Implement `Display` and `Debug` for enums that have a `.to_str()` method. +/// This ensures that the casing follows whatever `.to_str()` specifies. +macro_rules! impl_enum_display_and_debug_for_to_str { ($t:ident) => { -// // Constructors and accessors. -// impl $t { -// #[inline] -// pub fn new(value: String) -> Self { -// $t(value) -// } -// -// #[inline] -// pub fn new_from_str(value: &str) -> Self { -// $t(value.to_string()) -// } -// -// #[inline] -// pub fn as_str(&self) -> &str { -// &self.0 -// } -// } + + // Debug trait. + // Requires that the type has `.to_str()`. + impl std::fmt::Debug for $t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_str()) + } + } // Display trait. + // Requires that the type has `.to_str()`. impl std::fmt::Display for $t { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) + write!(f, "{}", self.to_str()) + } + } + + // NB: This test requires `strum::{EnumIter}` on the enum + #[cfg(test)] + #[test] + fn test_display_trait_matches_to_str() { + use strum::IntoEnumIterator; + for variant in $t::iter() { + assert_eq!(format!("{}", variant), variant.to_str()); + } + } + + // NB: This test requires `strum::{EnumIter}` on the enum + #[cfg(test)] + #[test] + fn test_debug_trait_matches_to_str() { + use strum::IntoEnumIterator; + for variant in $t::iter() { + assert_eq!(format!("{:?}", variant), variant.to_str()); } } } } + From a34aedc37528034fa7b0309f410bc44cd6b2c538 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 01:52:27 -0500 Subject: [PATCH 28/54] move sqlx impls to reusable macro --- crates/types/enums/src/core/visibility.rs | 2 +- .../enums/src/files/media_upload_type.rs | 40 +---------------- crates/types/enums/src/macros.rs | 44 ++++++++++++++++++- 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/crates/types/enums/src/core/visibility.rs b/crates/types/enums/src/core/visibility.rs index 7aa2c7e945..562b9d975b 100644 --- a/crates/types/enums/src/core/visibility.rs +++ b/crates/types/enums/src/core/visibility.rs @@ -37,7 +37,7 @@ pub enum Visibility { } -impl_enum_display_and_debug_for_to_str!(Visibility); +impl_enum_display_and_debug_using_to_str!(Visibility); // For reference, here's what the serde implementation might be if manually written. // This may be useful for designing composite types in the future: diff --git a/crates/types/enums/src/files/media_upload_type.rs b/crates/types/enums/src/files/media_upload_type.rs index d0b6c66237..7fd56e5c9f 100644 --- a/crates/types/enums/src/files/media_upload_type.rs +++ b/crates/types/enums/src/files/media_upload_type.rs @@ -1,12 +1,5 @@ use serde::Deserialize; use serde::Serialize; -use sqlx::Decode; -use sqlx::Encode; -use sqlx::Type; -use sqlx_core::database::HasArguments; -use sqlx_core::encode::IsNull; -use sqlx_core::error::BoxDynError; -use sqlx_core::mysql::{MySql, MySqlTypeInfo, MySqlValueRef}; #[cfg(test)] use strum::EnumCount; @@ -26,37 +19,8 @@ pub enum MediaUploadType { Video, } -// This overt approach is being taken because of the following error: -// `MySqlDatabaseError { code: Some("HY000"), number: 1210, message: "Incorrect arguments to mysqld_stmt_execute" }` -// Basically, sqlx can't turn our enum into a VARCHAR when using #[derive(sqlx::Type)]. -// We further lose the ability to `#[sqlx(rename_all="lowercase")]`, etc., so our encoder/decoder -// need to set the rules. -// Solution adapted from https://github.com/launchbadge/sqlx/discussions/1502 -impl Type for MediaUploadType { - fn type_info() -> MySqlTypeInfo { - String::type_info() - } -} - -impl<'q> Encode<'q, MySql> for MediaUploadType { - fn encode_by_ref(&self, buf: &mut >::ArgumentBuffer) -> IsNull { - // NB: In the absence of `#[derive(sqlx::Type)]` and `#sqlx(rename_all="lowercase")]`, - // this controls the casing of the variants when sent to MySQL. - self.to_str().encode_by_ref(buf) - } -} - -impl<'r> Decode<'r, MySql> for MediaUploadType { - fn decode( - value: MySqlValueRef<'r>, - ) -> Result { - let string = String::decode(value)?; - let value = MediaUploadType::from_str(&string)?; - Ok(value) - } -} - -impl_enum_display_and_debug_for_to_str!(MediaUploadType); +impl_enum_display_and_debug_using_to_str!(MediaUploadType); +impl_enum_sqlx_coders!(MediaUploadType); /// NB: Legacy API for older code. impl MediaUploadType { diff --git a/crates/types/enums/src/macros.rs b/crates/types/enums/src/macros.rs index 79f7694e1d..ede7c6970a 100644 --- a/crates/types/enums/src/macros.rs +++ b/crates/types/enums/src/macros.rs @@ -1,7 +1,7 @@ /// Implement `Display` and `Debug` for enums that have a `.to_str()` method. /// This ensures that the casing follows whatever `.to_str()` specifies. -macro_rules! impl_enum_display_and_debug_for_to_str { +macro_rules! impl_enum_display_and_debug_using_to_str { ($t:ident) => { // Debug trait. @@ -42,3 +42,45 @@ macro_rules! impl_enum_display_and_debug_for_to_str { } } +/// This overt approach is being taken because of the following error: +/// +/// `MySqlDatabaseError { code: Some("HY000"), number: 1210, message: "Incorrect arguments to mysqld_stmt_execute" }` +/// +/// Basically, sqlx can't turn our enum into a VARCHAR when using #[derive(sqlx::Type)]. +/// Unfortunately, by not using this, we also lose the ability to `#[sqlx(rename_all="lowercase")]`, etc., +/// so our encoder/decoder need to set the rules. +/// +/// Solution adapted from https://github.com/launchbadge/sqlx/discussions/1502 +macro_rules! impl_enum_sqlx_coders { + ($t:ident) => { + + impl sqlx::Type for $t { + fn type_info() -> sqlx_core::mysql::MySqlTypeInfo { + String::type_info() + } + } + + impl<'q> sqlx::Encode<'q, sqlx_core::mysql::MySql> for $t { + fn encode_by_ref( + &self, + buf: &mut >::ArgumentBuffer + ) -> sqlx_core::encode::IsNull { + // NB: In the absence of `#[derive(sqlx::Type)]` and `#sqlx(rename_all="lowercase")]`, + // this controls the casing of the variants when sent to MySQL. + self.to_str().encode_by_ref(buf) + } + } + + impl<'r> sqlx::Decode<'r, sqlx_core::mysql::MySql> for $t { + fn decode( + value: sqlx_core::mysql::MySqlValueRef<'r>, + ) -> Result { + let string = String::decode(value)?; + let value = $t::from_str(&string)?; + Ok(value) + } + } + + } +} + From 45b7af9bd25082fb05bf287cb9b7a3e33809bc85 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 02:32:56 -0500 Subject: [PATCH 29/54] detect upload mimetype --- Cargo.lock | 52 +++++++++++++++---- Cargo.toml | 3 +- .../media_uploads/insert_media_upload.rs | 39 ++++---------- crates/lib/files_common/Cargo.toml | 20 +++++++ crates/lib/files_common/src/lib.rs | 22 ++++++++ crates/lib/files_common/src/mimetype.rs | 13 +++++ crates/service/web/storyteller_web/Cargo.toml | 1 + .../endpoints/media_uploads/upload_audio.rs | 8 ++- .../enums/src/files/media_upload_type.rs | 1 + 9 files changed, 119 insertions(+), 40 deletions(-) create mode 100644 crates/lib/files_common/Cargo.toml create mode 100644 crates/lib/files_common/src/lib.rs create mode 100644 crates/lib/files_common/src/mimetype.rs diff --git a/Cargo.lock b/Cargo.lock index 1996857525..bf5b198dad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,7 +711,7 @@ dependencies = [ "smol_str", "thiserror", "tokio 1.16.1", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -1040,7 +1040,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca453e8624711b2f0f4eb47076a318feda166252a827ee25d067b43de83dcba0" dependencies = [ "byteorder", - "uuid", + "uuid 0.8.2", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid 1.2.2", ] [[package]] @@ -1210,7 +1221,7 @@ dependencies = [ "serde_json", "tempfile", "toml", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -1734,6 +1745,13 @@ dependencies = [ "instant", ] +[[package]] +name = "files_common" +version = "0.0.1" +dependencies = [ + "infer 0.11.0", +] + [[package]] name = "firestorm" version = "0.4.6" @@ -2423,7 +2441,7 @@ dependencies = [ "tts_common", "twitch_common", "user_input_common", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -2448,7 +2466,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea70330449622910e0edebab230734569516269fb32342fb0a8956340fa48c6c" dependencies = [ - "cfb", + "cfb 0.4.0", +] + +[[package]] +name = "infer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6c16b11a665b26aeeb9b1d7f954cdeb034be38dd00adab4f2ae921a8fee804" +dependencies = [ + "cfb 0.7.3", ] [[package]] @@ -3022,7 +3049,7 @@ dependencies = [ "serde", "serde_json", "tokio 1.16.1", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -4530,6 +4557,7 @@ dependencies = [ "dotenv", "easyenv", "enums", + "files_common", "futures 0.1.31", "futures 0.3.17", "futures-util", @@ -4584,7 +4612,7 @@ dependencies = [ "url_config", "user_input_common", "users_component", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -5344,7 +5372,7 @@ dependencies = [ "twitch_api2", "twitch_common", "twitch_oauth2", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -5606,6 +5634,12 @@ dependencies = [ "getrandom 0.2.2", ] +[[package]] +name = "uuid" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" + [[package]] name = "vcpkg" version = "0.2.12" @@ -5878,7 +5912,7 @@ dependencies = [ "twitch_api2", "twitch_common", "twitch_oauth2", - "uuid", + "uuid 0.8.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4c70aa854a..eb0241d11b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/lib/config", "crates/lib/container_common", "crates/lib/database_queries", + "crates/lib/files_common", "crates/lib/google_drive_common", "crates/lib/http_server_common", "crates/lib/jobs_common", @@ -28,7 +29,7 @@ members = [ # Types: common strongly-typed tokens, ids, payloads, etc. "crates/types/enums", - "crates/types/reusable_types", + "crates/types/reusable_types", # TODO(bt, 2022-12-21): Deprecate "crates/types/tokens", # Services: API servers, jobs, workers, etc. diff --git a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs index 3fbd1ba57c..6119a93059 100644 --- a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs +++ b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs @@ -36,33 +36,6 @@ pub struct Args <'a> { } pub async fn insert_media_upload(args: Args<'_>) -> AnyhowResult { - - /* - maybe_original_filename = ?, - original_file_size_bytes = ?, - original_duration_millis = ?, - maybe_original_mime_type = ?, - maybe_original_audio_encoding = ?, - maybe_original_video_encoding = ?, - maybe_original_frame_width = ?, - maybe_original_frame_height = ?, - - - - INSERT INTO - media_uploads - SET - token = 'foo', - uuid_idempotency_token = 'bar', - public_bucket_directory_full_path = 'asdf', - extra_file_modification_info = 'asdf', - maybe_creator_user_token = 'asdf', - maybe_creator_anonymous_visitor_token = 'asdf', - creator_ip_address = 'asdf', - creator_set_visibility = 'asdf', - maybe_creator_synthetic_id = 'asdf' - - */ let query = sqlx::query!( r#" INSERT INTO media_uploads @@ -71,6 +44,14 @@ SET uuid_idempotency_token = ?, media_type = ?, + maybe_original_filename = ?, + original_file_size_bytes = ?, + original_duration_millis = ?, + maybe_original_mime_type = ?, + maybe_original_audio_encoding = ?, + maybe_original_video_encoding = ?, + maybe_original_frame_width = ?, + maybe_original_frame_height = ?, checksum_sha2 = ?, public_bucket_directory_full_path = ?, @@ -87,14 +68,14 @@ SET args.uuid_idempotency_token, args.media_type, - /*args.maybe_original_filename, + args.maybe_original_filename, args.original_file_size_bytes, args.original_duration_millis, args.maybe_original_mime_type, args.maybe_original_audio_encoding, args.maybe_original_video_encoding, args.maybe_original_frame_width, - args.maybe_original_frame_height,*/ + args.maybe_original_frame_height, args.checksum_sha2, args.public_bucket_directory_full_path, diff --git a/crates/lib/files_common/Cargo.toml b/crates/lib/files_common/Cargo.toml new file mode 100644 index 0000000000..07311ba06a --- /dev/null +++ b/crates/lib/files_common/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "files_common" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +name = "files_common" +path = "src/lib.rs" + +[dependencies] +# Mimetype detection +infer = "0.11" + +[dev-dependencies] +# None yet diff --git a/crates/lib/files_common/src/lib.rs b/crates/lib/files_common/src/lib.rs new file mode 100644 index 0000000000..a66b0885cb --- /dev/null +++ b/crates/lib/files_common/src/lib.rs @@ -0,0 +1,22 @@ +//! files +//! +//! The purpose of this library is to have a strongly-typed MySQL enum-type wrapper. +//! This should also work for CHAR/VARCHAR fields that work similarly to enums (typically +//! as part of a composite key) +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +pub mod mimetype; diff --git a/crates/lib/files_common/src/mimetype.rs b/crates/lib/files_common/src/mimetype.rs new file mode 100644 index 0000000000..0a11467925 --- /dev/null +++ b/crates/lib/files_common/src/mimetype.rs @@ -0,0 +1,13 @@ + +const DEFAULT_BINARY_MIMETYPE : &'static str = "application/octet-stream"; + +pub fn get_mimetype_for_bytes(bytes: &[u8]) -> Option<&'static str> { + infer::get(bytes) + .map(|typ| typ.mime_type()) +} + +pub fn get_mimetype_for_bytes_or_default(bytes: &[u8]) -> &'static str { + infer::get(bytes) + .map(|typ| typ.mime_type()) + .unwrap_or(DEFAULT_BINARY_MIMETYPE) +} diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index b591342d1b..da0f5abf30 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -20,6 +20,7 @@ config = { path = "../../../lib/config" } container_common = { path = "../../../lib/container_common" } database_queries = { path = "../../../lib/database_queries" } enums = { path = "../../../types/enums" } +files_common = { path = "../../../lib/files_common" } google_drive_common = { path = "../../../lib/google_drive_common" } http_server_common = { path = "../../../lib/http_server_common" } jobs_common = { path = "../../../lib/jobs_common" } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index 47d2ac18f2..d2dbf95070 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -26,6 +26,7 @@ use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; use std::fmt; use std::sync::Arc; +use files_common::mimetype::{get_mimetype_for_bytes, get_mimetype_for_bytes_or_default}; use tokens::files::media_upload::MediaUploadToken; #[derive(Serialize)] @@ -132,6 +133,11 @@ pub async fn upload_audio_handler( let token = MediaUploadToken::generate(); + let maybe_mimetype = upload_media_request.file_bytes + .as_ref() + .map(|bytes| get_mimetype_for_bytes(bytes.as_ref())) + .flatten(); + let record_id = insert_media_upload(Args { token: &token, uuid_idempotency_token: &uuid_idempotency_token, @@ -139,7 +145,7 @@ pub async fn upload_audio_handler( maybe_original_filename: upload_media_request.file_name.as_deref(), original_file_size_bytes: 0, original_duration_millis: 0, - maybe_original_mime_type: None, + maybe_original_mime_type: maybe_mimetype, maybe_original_audio_encoding: None, maybe_original_video_encoding: None, maybe_original_frame_width: None, diff --git a/crates/types/enums/src/files/media_upload_type.rs b/crates/types/enums/src/files/media_upload_type.rs index 7fd56e5c9f..4e5d9d169d 100644 --- a/crates/types/enums/src/files/media_upload_type.rs +++ b/crates/types/enums/src/files/media_upload_type.rs @@ -19,6 +19,7 @@ pub enum MediaUploadType { Video, } +// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests! impl_enum_display_and_debug_using_to_str!(MediaUploadType); impl_enum_sqlx_coders!(MediaUploadType); From ff12990b01ee03109bd121b9e6d1b50f34bc43d2 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 02:46:40 -0500 Subject: [PATCH 30/54] sort _types crates at the top --- Cargo.toml | 10 +++++----- crates/{types => _types}/enums/Cargo.toml | 0 crates/{types => _types}/enums/src/core/mod.rs | 0 crates/{types => _types}/enums/src/core/visibility.rs | 0 .../enums/src/files/media_upload_type.rs | 0 crates/{types => _types}/enums/src/files/mod.rs | 0 crates/{types => _types}/enums/src/lib.rs | 0 crates/{types => _types}/enums/src/macros.rs | 0 crates/{types => _types}/enums/src/test_helpers.rs | 0 crates/{types => _types}/reusable_types/Cargo.toml | 0 .../reusable_types/src/achievements/mod.rs | 0 .../reusable_types/src/achievements/user_badge.rs | 0 .../reusable_types/src/achievements/user_badge_type.rs | 0 .../src/db/enums/generic_download_type.rs | 0 .../src/db/enums/generic_inference_type.rs | 0 .../reusable_types/src/db/enums/mod.rs | 0 .../reusable_types/src/db/enums/vocoder_type.rs | 0 crates/{types => _types}/reusable_types/src/db/mod.rs | 0 .../src/db/payloads/media_upload_details.rs | 0 .../reusable_types/src/db/payloads/mod.rs | 0 crates/{types => _types}/reusable_types/src/lib.rs | 0 .../reusable_types/src/server_environment.rs | 0 .../{types => _types}/reusable_types/src/stripe/mod.rs | 0 .../src/stripe/stripe_recurring_interval.rs | 0 .../src/stripe/stripe_subscription_status.rs | 0 .../reusable_types/src/test_helpers.rs | 0 crates/{types => _types}/tokens/Cargo.toml | 0 .../{types => _types}/tokens/src/files/media_upload.rs | 0 crates/{types => _types}/tokens/src/files/mod.rs | 0 crates/{types => _types}/tokens/src/jobs/download.rs | 0 crates/{types => _types}/tokens/src/jobs/inference.rs | 0 crates/{types => _types}/tokens/src/jobs/mod.rs | 0 crates/{types => _types}/tokens/src/lib.rs | 0 crates/{types => _types}/tokens/src/macros.rs | 0 crates/{types => _types}/tokens/src/prefixes.rs | 0 crates/{types => _types}/tokens/src/users/mod.rs | 0 crates/{types => _types}/tokens/src/users/user.rs | 0 .../tokens/src/voice_conversion/mod.rs | 0 .../tokens/src/voice_conversion/model.rs | 0 .../{types => _types}/tokens/tests/user_token_tests.rs | 0 crates/lib/database_queries/Cargo.toml | 6 +++--- crates/lib/http_server_common/Cargo.toml | 2 +- crates/lib/url_config/Cargo.toml | 2 +- crates/plugins/billing/Cargo.toml | 2 +- crates/plugins/users/Cargo.toml | 6 +++--- crates/service/job/download_job/Cargo.toml | 4 ++-- crates/service/web/image_generation_service/Cargo.toml | 2 +- crates/service/web/storyteller_web/Cargo.toml | 6 +++--- 48 files changed, 20 insertions(+), 20 deletions(-) rename crates/{types => _types}/enums/Cargo.toml (100%) rename crates/{types => _types}/enums/src/core/mod.rs (100%) rename crates/{types => _types}/enums/src/core/visibility.rs (100%) rename crates/{types => _types}/enums/src/files/media_upload_type.rs (100%) rename crates/{types => _types}/enums/src/files/mod.rs (100%) rename crates/{types => _types}/enums/src/lib.rs (100%) rename crates/{types => _types}/enums/src/macros.rs (100%) rename crates/{types => _types}/enums/src/test_helpers.rs (100%) rename crates/{types => _types}/reusable_types/Cargo.toml (100%) rename crates/{types => _types}/reusable_types/src/achievements/mod.rs (100%) rename crates/{types => _types}/reusable_types/src/achievements/user_badge.rs (100%) rename crates/{types => _types}/reusable_types/src/achievements/user_badge_type.rs (100%) rename crates/{types => _types}/reusable_types/src/db/enums/generic_download_type.rs (100%) rename crates/{types => _types}/reusable_types/src/db/enums/generic_inference_type.rs (100%) rename crates/{types => _types}/reusable_types/src/db/enums/mod.rs (100%) rename crates/{types => _types}/reusable_types/src/db/enums/vocoder_type.rs (100%) rename crates/{types => _types}/reusable_types/src/db/mod.rs (100%) rename crates/{types => _types}/reusable_types/src/db/payloads/media_upload_details.rs (100%) rename crates/{types => _types}/reusable_types/src/db/payloads/mod.rs (100%) rename crates/{types => _types}/reusable_types/src/lib.rs (100%) rename crates/{types => _types}/reusable_types/src/server_environment.rs (100%) rename crates/{types => _types}/reusable_types/src/stripe/mod.rs (100%) rename crates/{types => _types}/reusable_types/src/stripe/stripe_recurring_interval.rs (100%) rename crates/{types => _types}/reusable_types/src/stripe/stripe_subscription_status.rs (100%) rename crates/{types => _types}/reusable_types/src/test_helpers.rs (100%) rename crates/{types => _types}/tokens/Cargo.toml (100%) rename crates/{types => _types}/tokens/src/files/media_upload.rs (100%) rename crates/{types => _types}/tokens/src/files/mod.rs (100%) rename crates/{types => _types}/tokens/src/jobs/download.rs (100%) rename crates/{types => _types}/tokens/src/jobs/inference.rs (100%) rename crates/{types => _types}/tokens/src/jobs/mod.rs (100%) rename crates/{types => _types}/tokens/src/lib.rs (100%) rename crates/{types => _types}/tokens/src/macros.rs (100%) rename crates/{types => _types}/tokens/src/prefixes.rs (100%) rename crates/{types => _types}/tokens/src/users/mod.rs (100%) rename crates/{types => _types}/tokens/src/users/user.rs (100%) rename crates/{types => _types}/tokens/src/voice_conversion/mod.rs (100%) rename crates/{types => _types}/tokens/src/voice_conversion/model.rs (100%) rename crates/{types => _types}/tokens/tests/user_token_tests.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index eb0241d11b..9589ef0266 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,11 @@ [workspace] members = [ + # Types: common strongly-typed tokens, ids, payloads, etc. + "crates/_types/enums", + "crates/_types/reusable_types", # TODO(bt, 2022-12-21): Deprecate + "crates/_types/tokens", + # CLI tools for operations, etc. "crates/cli/database_backup", "crates/cli/database_cleanup", @@ -27,11 +32,6 @@ members = [ "crates/lib/url_config", "crates/lib/user_input_common", - # Types: common strongly-typed tokens, ids, payloads, etc. - "crates/types/enums", - "crates/types/reusable_types", # TODO(bt, 2022-12-21): Deprecate - "crates/types/tokens", - # Services: API servers, jobs, workers, etc. "crates/service/job/download_job", "crates/service/job/inference_job", diff --git a/crates/types/enums/Cargo.toml b/crates/_types/enums/Cargo.toml similarity index 100% rename from crates/types/enums/Cargo.toml rename to crates/_types/enums/Cargo.toml diff --git a/crates/types/enums/src/core/mod.rs b/crates/_types/enums/src/core/mod.rs similarity index 100% rename from crates/types/enums/src/core/mod.rs rename to crates/_types/enums/src/core/mod.rs diff --git a/crates/types/enums/src/core/visibility.rs b/crates/_types/enums/src/core/visibility.rs similarity index 100% rename from crates/types/enums/src/core/visibility.rs rename to crates/_types/enums/src/core/visibility.rs diff --git a/crates/types/enums/src/files/media_upload_type.rs b/crates/_types/enums/src/files/media_upload_type.rs similarity index 100% rename from crates/types/enums/src/files/media_upload_type.rs rename to crates/_types/enums/src/files/media_upload_type.rs diff --git a/crates/types/enums/src/files/mod.rs b/crates/_types/enums/src/files/mod.rs similarity index 100% rename from crates/types/enums/src/files/mod.rs rename to crates/_types/enums/src/files/mod.rs diff --git a/crates/types/enums/src/lib.rs b/crates/_types/enums/src/lib.rs similarity index 100% rename from crates/types/enums/src/lib.rs rename to crates/_types/enums/src/lib.rs diff --git a/crates/types/enums/src/macros.rs b/crates/_types/enums/src/macros.rs similarity index 100% rename from crates/types/enums/src/macros.rs rename to crates/_types/enums/src/macros.rs diff --git a/crates/types/enums/src/test_helpers.rs b/crates/_types/enums/src/test_helpers.rs similarity index 100% rename from crates/types/enums/src/test_helpers.rs rename to crates/_types/enums/src/test_helpers.rs diff --git a/crates/types/reusable_types/Cargo.toml b/crates/_types/reusable_types/Cargo.toml similarity index 100% rename from crates/types/reusable_types/Cargo.toml rename to crates/_types/reusable_types/Cargo.toml diff --git a/crates/types/reusable_types/src/achievements/mod.rs b/crates/_types/reusable_types/src/achievements/mod.rs similarity index 100% rename from crates/types/reusable_types/src/achievements/mod.rs rename to crates/_types/reusable_types/src/achievements/mod.rs diff --git a/crates/types/reusable_types/src/achievements/user_badge.rs b/crates/_types/reusable_types/src/achievements/user_badge.rs similarity index 100% rename from crates/types/reusable_types/src/achievements/user_badge.rs rename to crates/_types/reusable_types/src/achievements/user_badge.rs diff --git a/crates/types/reusable_types/src/achievements/user_badge_type.rs b/crates/_types/reusable_types/src/achievements/user_badge_type.rs similarity index 100% rename from crates/types/reusable_types/src/achievements/user_badge_type.rs rename to crates/_types/reusable_types/src/achievements/user_badge_type.rs diff --git a/crates/types/reusable_types/src/db/enums/generic_download_type.rs b/crates/_types/reusable_types/src/db/enums/generic_download_type.rs similarity index 100% rename from crates/types/reusable_types/src/db/enums/generic_download_type.rs rename to crates/_types/reusable_types/src/db/enums/generic_download_type.rs diff --git a/crates/types/reusable_types/src/db/enums/generic_inference_type.rs b/crates/_types/reusable_types/src/db/enums/generic_inference_type.rs similarity index 100% rename from crates/types/reusable_types/src/db/enums/generic_inference_type.rs rename to crates/_types/reusable_types/src/db/enums/generic_inference_type.rs diff --git a/crates/types/reusable_types/src/db/enums/mod.rs b/crates/_types/reusable_types/src/db/enums/mod.rs similarity index 100% rename from crates/types/reusable_types/src/db/enums/mod.rs rename to crates/_types/reusable_types/src/db/enums/mod.rs diff --git a/crates/types/reusable_types/src/db/enums/vocoder_type.rs b/crates/_types/reusable_types/src/db/enums/vocoder_type.rs similarity index 100% rename from crates/types/reusable_types/src/db/enums/vocoder_type.rs rename to crates/_types/reusable_types/src/db/enums/vocoder_type.rs diff --git a/crates/types/reusable_types/src/db/mod.rs b/crates/_types/reusable_types/src/db/mod.rs similarity index 100% rename from crates/types/reusable_types/src/db/mod.rs rename to crates/_types/reusable_types/src/db/mod.rs diff --git a/crates/types/reusable_types/src/db/payloads/media_upload_details.rs b/crates/_types/reusable_types/src/db/payloads/media_upload_details.rs similarity index 100% rename from crates/types/reusable_types/src/db/payloads/media_upload_details.rs rename to crates/_types/reusable_types/src/db/payloads/media_upload_details.rs diff --git a/crates/types/reusable_types/src/db/payloads/mod.rs b/crates/_types/reusable_types/src/db/payloads/mod.rs similarity index 100% rename from crates/types/reusable_types/src/db/payloads/mod.rs rename to crates/_types/reusable_types/src/db/payloads/mod.rs diff --git a/crates/types/reusable_types/src/lib.rs b/crates/_types/reusable_types/src/lib.rs similarity index 100% rename from crates/types/reusable_types/src/lib.rs rename to crates/_types/reusable_types/src/lib.rs diff --git a/crates/types/reusable_types/src/server_environment.rs b/crates/_types/reusable_types/src/server_environment.rs similarity index 100% rename from crates/types/reusable_types/src/server_environment.rs rename to crates/_types/reusable_types/src/server_environment.rs diff --git a/crates/types/reusable_types/src/stripe/mod.rs b/crates/_types/reusable_types/src/stripe/mod.rs similarity index 100% rename from crates/types/reusable_types/src/stripe/mod.rs rename to crates/_types/reusable_types/src/stripe/mod.rs diff --git a/crates/types/reusable_types/src/stripe/stripe_recurring_interval.rs b/crates/_types/reusable_types/src/stripe/stripe_recurring_interval.rs similarity index 100% rename from crates/types/reusable_types/src/stripe/stripe_recurring_interval.rs rename to crates/_types/reusable_types/src/stripe/stripe_recurring_interval.rs diff --git a/crates/types/reusable_types/src/stripe/stripe_subscription_status.rs b/crates/_types/reusable_types/src/stripe/stripe_subscription_status.rs similarity index 100% rename from crates/types/reusable_types/src/stripe/stripe_subscription_status.rs rename to crates/_types/reusable_types/src/stripe/stripe_subscription_status.rs diff --git a/crates/types/reusable_types/src/test_helpers.rs b/crates/_types/reusable_types/src/test_helpers.rs similarity index 100% rename from crates/types/reusable_types/src/test_helpers.rs rename to crates/_types/reusable_types/src/test_helpers.rs diff --git a/crates/types/tokens/Cargo.toml b/crates/_types/tokens/Cargo.toml similarity index 100% rename from crates/types/tokens/Cargo.toml rename to crates/_types/tokens/Cargo.toml diff --git a/crates/types/tokens/src/files/media_upload.rs b/crates/_types/tokens/src/files/media_upload.rs similarity index 100% rename from crates/types/tokens/src/files/media_upload.rs rename to crates/_types/tokens/src/files/media_upload.rs diff --git a/crates/types/tokens/src/files/mod.rs b/crates/_types/tokens/src/files/mod.rs similarity index 100% rename from crates/types/tokens/src/files/mod.rs rename to crates/_types/tokens/src/files/mod.rs diff --git a/crates/types/tokens/src/jobs/download.rs b/crates/_types/tokens/src/jobs/download.rs similarity index 100% rename from crates/types/tokens/src/jobs/download.rs rename to crates/_types/tokens/src/jobs/download.rs diff --git a/crates/types/tokens/src/jobs/inference.rs b/crates/_types/tokens/src/jobs/inference.rs similarity index 100% rename from crates/types/tokens/src/jobs/inference.rs rename to crates/_types/tokens/src/jobs/inference.rs diff --git a/crates/types/tokens/src/jobs/mod.rs b/crates/_types/tokens/src/jobs/mod.rs similarity index 100% rename from crates/types/tokens/src/jobs/mod.rs rename to crates/_types/tokens/src/jobs/mod.rs diff --git a/crates/types/tokens/src/lib.rs b/crates/_types/tokens/src/lib.rs similarity index 100% rename from crates/types/tokens/src/lib.rs rename to crates/_types/tokens/src/lib.rs diff --git a/crates/types/tokens/src/macros.rs b/crates/_types/tokens/src/macros.rs similarity index 100% rename from crates/types/tokens/src/macros.rs rename to crates/_types/tokens/src/macros.rs diff --git a/crates/types/tokens/src/prefixes.rs b/crates/_types/tokens/src/prefixes.rs similarity index 100% rename from crates/types/tokens/src/prefixes.rs rename to crates/_types/tokens/src/prefixes.rs diff --git a/crates/types/tokens/src/users/mod.rs b/crates/_types/tokens/src/users/mod.rs similarity index 100% rename from crates/types/tokens/src/users/mod.rs rename to crates/_types/tokens/src/users/mod.rs diff --git a/crates/types/tokens/src/users/user.rs b/crates/_types/tokens/src/users/user.rs similarity index 100% rename from crates/types/tokens/src/users/user.rs rename to crates/_types/tokens/src/users/user.rs diff --git a/crates/types/tokens/src/voice_conversion/mod.rs b/crates/_types/tokens/src/voice_conversion/mod.rs similarity index 100% rename from crates/types/tokens/src/voice_conversion/mod.rs rename to crates/_types/tokens/src/voice_conversion/mod.rs diff --git a/crates/types/tokens/src/voice_conversion/model.rs b/crates/_types/tokens/src/voice_conversion/model.rs similarity index 100% rename from crates/types/tokens/src/voice_conversion/model.rs rename to crates/_types/tokens/src/voice_conversion/model.rs diff --git a/crates/types/tokens/tests/user_token_tests.rs b/crates/_types/tokens/tests/user_token_tests.rs similarity index 100% rename from crates/types/tokens/tests/user_token_tests.rs rename to crates/_types/tokens/tests/user_token_tests.rs diff --git a/crates/lib/database_queries/Cargo.toml b/crates/lib/database_queries/Cargo.toml index 1e9854bd09..17f0537c4a 100644 --- a/crates/lib/database_queries/Cargo.toml +++ b/crates/lib/database_queries/Cargo.toml @@ -17,9 +17,9 @@ path = "src/lib.rs" # Internal config = { path = "../config" } container_common = { path = "../container_common" } -enums = { path = "../../types/enums" } -reusable_types = { path = "../../types/reusable_types" } -tokens = { path = "../../types/tokens" } +enums = { path = "../../_types/enums" } +reusable_types = { path = "../../_types/reusable_types" } +tokens = { path = "../../_types/tokens" } # External anyhow = "1.0.40" diff --git a/crates/lib/http_server_common/Cargo.toml b/crates/lib/http_server_common/Cargo.toml index c570de11d5..10554fd2a9 100644 --- a/crates/lib/http_server_common/Cargo.toml +++ b/crates/lib/http_server_common/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" # Internal container_common = { path = "../container_common" } -reusable_types = { path = "../../types/reusable_types" } +reusable_types = { path = "../../_types/reusable_types" } # External anyhow = "1.0.40" diff --git a/crates/lib/url_config/Cargo.toml b/crates/lib/url_config/Cargo.toml index 88bad19e0e..11f643a027 100644 --- a/crates/lib/url_config/Cargo.toml +++ b/crates/lib/url_config/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" # Internal http_server_common = { path = "../../lib/http_server_common" } -reusable_types = { path = "../../types/reusable_types" } +reusable_types = { path = "../../_types/reusable_types" } # External actix-web = "4.0.0-beta.10" diff --git a/crates/plugins/billing/Cargo.toml b/crates/plugins/billing/Cargo.toml index ed92473329..dae42be476 100644 --- a/crates/plugins/billing/Cargo.toml +++ b/crates/plugins/billing/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" container_common = { path = "../../lib/container_common" } database_queries = { path = "../../lib/database_queries" } http_server_common = { path = "../../lib/http_server_common" } -reusable_types = { path = "../../types/reusable_types" } +reusable_types = { path = "../../_types/reusable_types" } url_config = { path = "../../lib/url_config" } # External diff --git a/crates/plugins/users/Cargo.toml b/crates/plugins/users/Cargo.toml index a072e348bf..d798745426 100644 --- a/crates/plugins/users/Cargo.toml +++ b/crates/plugins/users/Cargo.toml @@ -17,10 +17,10 @@ path = "src/lib.rs" # Internal container_common = { path = "../../lib/container_common" } database_queries = { path = "../../lib/database_queries" } -enums = { path = "../../types/enums" } +enums = { path = "../../_types/enums" } http_server_common = { path = "../../lib/http_server_common" } -reusable_types = { path = "../../types/reusable_types" } -tokens = { path = "../../types/tokens" } +reusable_types = { path = "../../_types/reusable_types" } +tokens = { path = "../../_types/tokens" } user_input_common = { path = "../../lib/user_input_common" } # External diff --git a/crates/service/job/download_job/Cargo.toml b/crates/service/job/download_job/Cargo.toml index 39196b6692..486b65038e 100644 --- a/crates/service/job/download_job/Cargo.toml +++ b/crates/service/job/download_job/Cargo.toml @@ -18,10 +18,10 @@ path = "src/main.rs" config = { path = "../../../lib/config" } container_common = { path = "../../../lib/container_common" } database_queries = { path = "../../../lib/database_queries" } -enums = { path = "../../../types/enums" } +enums = { path = "../../../_types/enums" } google_drive_common = { path = "../../../lib/google_drive_common" } jobs_common = { path = "../../../lib/jobs_common" } -reusable_types = { path = "../../../types/reusable_types" } +reusable_types = { path = "../../../_types/reusable_types" } storage_buckets_common = { path = "../../../lib/storage_buckets_common" } subprocess_common = { path = "../../../lib/subprocess_common" } tts_common = { path = "../../../lib/tts_common" } diff --git a/crates/service/web/image_generation_service/Cargo.toml b/crates/service/web/image_generation_service/Cargo.toml index 9cca2972e2..2b2a7a51bb 100644 --- a/crates/service/web/image_generation_service/Cargo.toml +++ b/crates/service/web/image_generation_service/Cargo.toml @@ -24,7 +24,7 @@ http_server_common = { path = "../../../lib/http_server_common" } jobs_common = { path = "../../../lib/jobs_common" } memory_caching = { path = "../../../lib/memory_caching" } redis_common = { path = "../../../lib/redis_common" } -reusable_types = { path = "../../../types/reusable_types" } +reusable_types = { path = "../../../_types/reusable_types" } storage_buckets_common = { path = "../../../lib/storage_buckets_common" } tts_common = { path = "../../../lib/tts_common" } twitch_common = { path = "../../../lib/twitch_common" } diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index da0f5abf30..a27b0cbc9a 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -19,16 +19,16 @@ path = "src/main.rs" config = { path = "../../../lib/config" } container_common = { path = "../../../lib/container_common" } database_queries = { path = "../../../lib/database_queries" } -enums = { path = "../../../types/enums" } +enums = { path = "../../../_types/enums" } files_common = { path = "../../../lib/files_common" } google_drive_common = { path = "../../../lib/google_drive_common" } http_server_common = { path = "../../../lib/http_server_common" } jobs_common = { path = "../../../lib/jobs_common" } memory_caching = { path = "../../../lib/memory_caching" } redis_common = { path = "../../../lib/redis_common" } -reusable_types = { path = "../../../types/reusable_types" } +reusable_types = { path = "../../../_types/reusable_types" } storage_buckets_common = { path = "../../../lib/storage_buckets_common" } -tokens = { path = "../../../types/tokens" } +tokens = { path = "../../../_types/tokens" } tts_common = { path = "../../../lib/tts_common" } twitch_common = { path = "../../../lib/twitch_common" } url_config = { path = "../../../lib/url_config" } From 9aaee506ede11fa4fb5d05c42e20efb2a7c80948 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 03:15:23 -0500 Subject: [PATCH 31/54] hash uploaded file --- Cargo.lock | 16 ++++---- crates/lib/files_common/Cargo.toml | 5 ++- crates/lib/files_common/src/hash.rs | 39 +++++++++++++++++++ crates/lib/files_common/src/lib.rs | 1 + .../endpoints/media_uploads/upload_audio.rs | 17 +++++++- 5 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 crates/lib/files_common/src/hash.rs diff --git a/Cargo.lock b/Cargo.lock index bf5b198dad..97e96dfa2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1216,7 +1216,7 @@ dependencies = [ "log", "once_cell", "rand 0.8.4", - "ring 0.17.0-alpha.10", + "ring 0.17.0-alpha.11", "serde", "serde_json", "tempfile", @@ -1426,9 +1426,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" [[package]] name = "database-backup" @@ -1749,7 +1749,9 @@ dependencies = [ name = "files_common" version = "0.0.1" dependencies = [ + "data-encoding", "infer 0.11.0", + "ring 0.17.0-alpha.11", ] [[package]] @@ -2425,7 +2427,7 @@ dependencies = [ "regex", "reqwest", "reusable_types", - "ring 0.17.0-alpha.10", + "ring 0.17.0-alpha.11", "rust-s3", "serde", "serde_derive", @@ -3869,9 +3871,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.0-alpha.10" +version = "0.17.0-alpha.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ae9a4d2975bdd8254d7bcdd2261be62b42b8920f903ec682b08cdc11b87af2" +checksum = "4575a179070909595bea5f999d67934737c2e0757a1eb9839af555917817b257" dependencies = [ "cc", "libc", @@ -4590,7 +4592,7 @@ dependencies = [ "regex", "reqwest", "reusable_types", - "ring 0.17.0-alpha.10", + "ring 0.17.0-alpha.11", "rust-s3", "serde", "serde_derive", diff --git a/crates/lib/files_common/Cargo.toml b/crates/lib/files_common/Cargo.toml index 07311ba06a..8f1d525e62 100644 --- a/crates/lib/files_common/Cargo.toml +++ b/crates/lib/files_common/Cargo.toml @@ -13,8 +13,9 @@ name = "files_common" path = "src/lib.rs" [dependencies] -# Mimetype detection -infer = "0.11" +data-encoding = "2.3.3" # base64, hex, etc. +infer = "0.11" # Mimetype detection +ring = "0.17.0-alpha.11" # Crypto [dev-dependencies] # None yet diff --git a/crates/lib/files_common/src/hash.rs b/crates/lib/files_common/src/hash.rs new file mode 100644 index 0000000000..450f8374dd --- /dev/null +++ b/crates/lib/files_common/src/hash.rs @@ -0,0 +1,39 @@ +use data_encoding::HEXLOWER_PERMISSIVE; +use ring::digest::{Context, Digest, SHA256}; +use std::io::{BufReader, Read}; + +pub fn hash_bytes_sha2(bytes: &[u8]) -> std::io::Result { + let reader = BufReader::new(bytes); + let digest = sha256_digest(reader)?; + let hash = HEXLOWER_PERMISSIVE.encode(digest.as_ref()); + Ok(hash) +} + +fn sha256_digest(mut reader: R) -> std::io::Result { + let mut context = Context::new(&SHA256); + let mut buffer = [0; 1024]; + + loop { + let count = reader.read(&mut buffer)?; + if count == 0 { + break; + } + context.update(&buffer[..count]); + } + + Ok(context.finish()) +} + +#[cfg(test)] +mod tests { + use crate::hash::hash_bytes_sha2; + + #[test] + fn test_hash_bytes_sha2() { + assert_eq!(hash_bytes_sha2(&[]).unwrap(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + assert_eq!(hash_bytes_sha2(&[0, 0, 0, 0]).unwrap(), "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"); + assert_eq!(hash_bytes_sha2(&[0, 255, 0, 255]).unwrap(), "7a7bf454c5f3cb1b9d9a20f81417f98d976fe3b3dd52c1b9968f02e89e7e8a2f"); + assert_eq!(hash_bytes_sha2("testing".as_bytes()).unwrap(), "cf80cd8aed482d5d1527d7dc72fceff84e6326592848447d2dc0b0e87dfc9a90"); + assert_eq!(hash_bytes_sha2("hash this".as_bytes()).unwrap(), "19467788bc0cf11790a075ea718452cecf0e79db59d1964670475e5fe2e4a611"); + } +} diff --git a/crates/lib/files_common/src/lib.rs b/crates/lib/files_common/src/lib.rs index a66b0885cb..fb5bf60d61 100644 --- a/crates/lib/files_common/src/lib.rs +++ b/crates/lib/files_common/src/lib.rs @@ -19,4 +19,5 @@ #![allow(dead_code)] #![allow(non_snake_case)] +pub mod hash; pub mod mimetype; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index d2dbf95070..8b493586a4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -26,6 +26,7 @@ use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; use std::fmt; use std::sync::Arc; +use files_common::hash::hash_bytes_sha2; use files_common::mimetype::{get_mimetype_for_bytes, get_mimetype_for_bytes_or_default}; use tokens::files::media_upload::MediaUploadToken; @@ -138,6 +139,20 @@ pub async fn upload_audio_handler( .map(|bytes| get_mimetype_for_bytes(bytes.as_ref())) .flatten(); + let maybe_hash = upload_media_request.file_bytes + .as_ref() + .map(|bytes| hash_bytes_sha2(bytes.as_ref())) + .transpose() + .map_err(|io_error| { + warn!("Problem hashing bytes: {:?}", io_error); + return UploadAudioError::ServerError; + })?; + + let hash = match maybe_hash { + None => return Err(UploadAudioError::BadInput("invalid file".to_string())), + Some(hash) => hash, + }; + let record_id = insert_media_upload(Args { token: &token, uuid_idempotency_token: &uuid_idempotency_token, @@ -150,7 +165,7 @@ pub async fn upload_audio_handler( maybe_original_video_encoding: None, maybe_original_frame_width: None, maybe_original_frame_height: None, - checksum_sha2: "", + checksum_sha2: &hash, public_bucket_directory_full_path: "", extra_file_modification_info: MediaUploadDetails {}, // TODO maybe_creator_user_token: maybe_user_token.as_ref(), From c4c5b8911872c9bef8754e2f6c500c49b1d9b03d Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 04:34:18 -0500 Subject: [PATCH 32/54] Calculate audio duration millis --- Cargo.lock | 136 +++++++++++++++++- .../media_uploads/insert_media_upload.rs | 4 +- crates/service/web/storyteller_web/Cargo.toml | 1 + .../endpoints/media_uploads/upload_audio.rs | 66 ++++++++- 4 files changed, 200 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97e96dfa2a..0b456d923d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -667,6 +667,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "ascii" version = "0.9.3" @@ -981,6 +987,12 @@ version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" +[[package]] +name = "bytemuck" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" + [[package]] name = "byteorder" version = "1.4.3" @@ -2651,7 +2663,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", "bitflags", "cfg-if 1.0.0", "ryu", @@ -4602,6 +4614,7 @@ dependencies = [ "sqlx", "storage_buckets_common", "subprocess", + "symphonia", "tempdir", "time 0.2.26", "tokens", @@ -4685,6 +4698,127 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +[[package]] +name = "symphonia" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17033fe05e4f7f10a6ad602c272bafd2520b2e5cdd9feb61494d9cdce08e002f" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-wav", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044f655337892b217d6df1d8336ee119414c3886c89c72e0156989cd2ad7934a" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cca412c954abda6ab62b5e51223568eb604ed8266ec777d99e1d63c608443c" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323b94435a1a807e1001e29490aeaef2660fb72b145d47497e8429a6cb1d67c3" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199a6417cd4115bac79289b64b859358ea050b7add0ceb364dc991f628c5b347" +dependencies = [ + "arrayvec 0.7.2", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f405400330b2cc0c70e19515198628ae9a6a99a59c77800541127d4cff113b96" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2f741469a0f103607ed1f2605f7f00b13ba044ea9ddc616764558c6d3d9b7d" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-wav" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9d771aa6889f05b771e629110f6a60b5e1c0ad580fc41da574bc490fbe2822" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed71acf6b5e6e8bee1509597b86365a06b78c1d73218df47357620a6fe5997b" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbb0766ce77a8aef535f9438db645e7b6f1b2c4cf3be9bf246b4e11a7d5531" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.103" diff --git a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs index 6119a93059..747ebe8aa3 100644 --- a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs +++ b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs @@ -14,7 +14,7 @@ pub struct Args <'a> { pub media_type: MediaUploadType, pub maybe_original_filename: Option<&'a str>, pub original_file_size_bytes: u64, - pub original_duration_millis: u64, + pub maybe_original_duration_millis: Option, pub maybe_original_mime_type: Option<&'a str>, pub maybe_original_audio_encoding: Option<&'a str>, pub maybe_original_video_encoding: Option<&'a str>, @@ -70,7 +70,7 @@ SET args.media_type, args.maybe_original_filename, args.original_file_size_bytes, - args.original_duration_millis, + args.maybe_original_duration_millis, args.maybe_original_mime_type, args.maybe_original_audio_encoding, args.maybe_original_video_encoding, diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index a27b0cbc9a..68d7d2850e 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -57,6 +57,7 @@ log = "0.4.14" once_cell = "1.9.0" rand = "0.8.3" regex = "1.5.4" +symphonia = "0.5.1" # Rust audio demuxer time = "0.2.26" uuid = { version = "0.8.2", features = ["v4"] } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index 8b493586a4..79e30fece2 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -5,6 +5,7 @@ use actix_web::HttpResponseBuilder; use actix_web::cookie::Cookie; use actix_web::error::ResponseError; use actix_web::http::StatusCode; +use actix_web::web::BytesMut; use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; use config::bad_urls::is_bad_tts_model_download_url; use crate::http_server::endpoints::media_uploads::drain_multipart_request::drain_multipart_request; @@ -14,20 +15,25 @@ use crate::validations::model_uploads::validate_model_title; use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; use database_queries::queries::media_uploads::insert_media_upload::{Args, insert_media_upload}; use enums::core::visibility::Visibility; +use enums::files::media_upload_type::MediaUploadType; +use files_common::hash::hash_bytes_sha2; +use files_common::mimetype::{get_mimetype_for_bytes, get_mimetype_for_bytes_or_default}; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; use regex::Regex; use reusable_types::db::enums::generic_download_type::GenericDownloadType; -use enums::files::media_upload_type::MediaUploadType; use reusable_types::db::payloads::MediaUploadDetails; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; use std::fmt; +use std::io::{BufReader, Cursor}; use std::sync::Arc; -use files_common::hash::hash_bytes_sha2; -use files_common::mimetype::{get_mimetype_for_bytes, get_mimetype_for_bytes_or_default}; +use symphonia::core::formats::FormatOptions; +use symphonia::core::io::{MediaSourceStream, ReadOnlySource}; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; use tokens::files::media_upload::MediaUploadToken; #[derive(Serialize)] @@ -153,13 +159,65 @@ pub async fn upload_audio_handler( Some(hash) => hash, }; + let bytes = match upload_media_request.file_bytes { + None => return Err(UploadAudioError::BadInput("invalid file".to_string())), + Some(bytes) => bytes, + }; + + let mut maybe_duration_millis = None; + + if let Some(mimetype) = maybe_mimetype.as_deref() { + match mimetype { + "audio/x-wav" => { + // FIXME(bt, 2022-12-21): Ugh. + let bytes = bytes.to_vec(); + let reader = Cursor::new(bytes); + let source = ReadOnlySource::new(reader); + let mss = MediaSourceStream::new(Box::new(source), Default::default()); + + // Create a probe hint using the file's extension. [Optional] + let mut hint = Hint::new(); + hint.mime_type(mimetype); + + // Use the default options for metadata and format readers. + let meta_opts: MetadataOptions = Default::default(); + let fmt_opts: FormatOptions = Default::default(); + + // Probe the media source. + let probed = symphonia::default::get_probe() + .format(&hint, mss, &fmt_opts, &meta_opts) + .map_err(|e| { + warn!("Invalid format: {:?}", e); + UploadAudioError::BadInput("bad file".to_string()) + })?; + + let format = probed.format; + + let duration = format.default_track() + .map(|track| { + let timebase= track.codec_params.time_base.unwrap(); + let n_frames = track.codec_params.n_frames.unwrap(); + timebase.calc_time(n_frames) + }) + .unwrap(); + + let duration_millis = duration.seconds * 1000; + let frac_millis = (duration.frac * 1000.0).trunc() as u64; + let duration_millis = duration_millis + frac_millis; + + maybe_duration_millis = Some(duration_millis); + } + _ => {} + } + } + let record_id = insert_media_upload(Args { token: &token, uuid_idempotency_token: &uuid_idempotency_token, media_type: MediaUploadType::Audio, maybe_original_filename: upload_media_request.file_name.as_deref(), original_file_size_bytes: 0, - original_duration_millis: 0, + maybe_original_duration_millis: maybe_duration_millis, maybe_original_mime_type: maybe_mimetype, maybe_original_audio_encoding: None, maybe_original_video_encoding: None, From 1ba32158815707e322764ebdce975de1e599a664 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 05:19:43 -0500 Subject: [PATCH 33/54] organize media/files crates --- Cargo.lock | 12 +++- Cargo.toml | 3 +- crates/lib/{files_common => files}/Cargo.toml | 6 +- .../lib/{files_common => files}/src/hash.rs | 0 crates/lib/{files_common => files}/src/lib.rs | 4 +- crates/lib/files/src/mimetype.rs | 41 +++++++++++++ crates/lib/files_common/src/mimetype.rs | 13 ----- crates/lib/media/Cargo.toml | 19 +++++++ .../lib/media/src/decode_basic_audio_info.rs | 57 +++++++++++++++++++ crates/lib/media/src/lib.rs | 21 +++++++ crates/service/web/storyteller_web/Cargo.toml | 3 +- .../endpoints/media_uploads/upload_audio.rs | 15 ++++- 12 files changed, 169 insertions(+), 25 deletions(-) rename crates/lib/{files_common => files}/Cargo.toml (70%) rename crates/lib/{files_common => files}/src/hash.rs (100%) rename crates/lib/{files_common => files}/src/lib.rs (64%) create mode 100644 crates/lib/files/src/mimetype.rs delete mode 100644 crates/lib/files_common/src/mimetype.rs create mode 100644 crates/lib/media/Cargo.toml create mode 100644 crates/lib/media/src/decode_basic_audio_info.rs create mode 100644 crates/lib/media/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 0b456d923d..a91df1b9c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1758,7 +1758,7 @@ dependencies = [ ] [[package]] -name = "files_common" +name = "files" version = "0.0.1" dependencies = [ "data-encoding", @@ -2856,6 +2856,13 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "media" +version = "0.0.1" +dependencies = [ + "symphonia", +] + [[package]] name = "memchr" version = "2.4.0" @@ -4571,7 +4578,7 @@ dependencies = [ "dotenv", "easyenv", "enums", - "files_common", + "files", "futures 0.1.31", "futures 0.3.17", "futures-util", @@ -4593,6 +4600,7 @@ dependencies = [ "lru_time_cache", "magic-crypt", "md-5", + "media", "memory_caching", "native-tls", "newrelic-telemetry", diff --git a/Cargo.toml b/Cargo.toml index 9589ef0266..90b196f73e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,10 +19,11 @@ members = [ "crates/lib/config", "crates/lib/container_common", "crates/lib/database_queries", - "crates/lib/files_common", + "crates/lib/files", "crates/lib/google_drive_common", "crates/lib/http_server_common", "crates/lib/jobs_common", + "crates/lib/media", "crates/lib/memory_caching", "crates/lib/redis_common", "crates/lib/storage_buckets_common", diff --git a/crates/lib/files_common/Cargo.toml b/crates/lib/files/Cargo.toml similarity index 70% rename from crates/lib/files_common/Cargo.toml rename to crates/lib/files/Cargo.toml index 8f1d525e62..3fa52dadda 100644 --- a/crates/lib/files_common/Cargo.toml +++ b/crates/lib/files/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "files_common" +name = "files" edition = "2018" version = "0.0.1" authors = [ @@ -9,7 +9,9 @@ authors = [ publish = false [lib] -name = "files_common" +# NB: Public "files" crate is some CDN nonsense we'll never use, so we're good to +# use this crate name internally. +name = "files" path = "src/lib.rs" [dependencies] diff --git a/crates/lib/files_common/src/hash.rs b/crates/lib/files/src/hash.rs similarity index 100% rename from crates/lib/files_common/src/hash.rs rename to crates/lib/files/src/hash.rs diff --git a/crates/lib/files_common/src/lib.rs b/crates/lib/files/src/lib.rs similarity index 64% rename from crates/lib/files_common/src/lib.rs rename to crates/lib/files/src/lib.rs index fb5bf60d61..63673830ad 100644 --- a/crates/lib/files_common/src/lib.rs +++ b/crates/lib/files/src/lib.rs @@ -1,8 +1,6 @@ //! files //! -//! The purpose of this library is to have a strongly-typed MySQL enum-type wrapper. -//! This should also work for CHAR/VARCHAR fields that work similarly to enums (typically -//! as part of a composite key) +//! The purpose of this library is to group common file (or in-memory file) operations. //! // Never allow these diff --git a/crates/lib/files/src/mimetype.rs b/crates/lib/files/src/mimetype.rs new file mode 100644 index 0000000000..444515c9ba --- /dev/null +++ b/crates/lib/files/src/mimetype.rs @@ -0,0 +1,41 @@ + +const DEFAULT_BINARY_MIMETYPE : &'static str = "application/octet-stream"; + +pub fn get_mimetype_for_bytes(bytes: &[u8]) -> Option<&'static str> { + infer::get(bytes) + .map(|typ| typ.mime_type()) +} + +pub fn get_mimetype_for_bytes_or_default(bytes: &[u8]) -> &'static str { + infer::get(bytes) + .map(|typ| typ.mime_type()) + .unwrap_or(DEFAULT_BINARY_MIMETYPE) +} + +#[cfg(test)] +mod tests { + use crate::mimetype::{get_mimetype_for_bytes, get_mimetype_for_bytes_or_default}; + + #[test] + fn test_mimetype_png() { + // NB: From "Hanashi Mask.png" + let bytes : Vec = vec![ + 137, 80, 78, 71, 13, 10, 26, 10, + 0, 0, 0, 13, 73, 72, 68, 82, + 0, 0, 20, 193, 0, 0, 15, 239, + 8, 6, 0, 0, 0, 163, 131, 119, + 122, 0, 0, 0, + ]; + + assert_eq!(get_mimetype_for_bytes(&bytes), Some("image/png")); + assert_eq!(get_mimetype_for_bytes_or_default(&bytes), "image/png"); + } + + #[test] + fn test_mimetype_unknown() { + let bytes : Vec = vec![]; + + assert_eq!(get_mimetype_for_bytes(&bytes), None); + assert_eq!(get_mimetype_for_bytes_or_default(&bytes), "application/octet-stream"); + } +} diff --git a/crates/lib/files_common/src/mimetype.rs b/crates/lib/files_common/src/mimetype.rs deleted file mode 100644 index 0a11467925..0000000000 --- a/crates/lib/files_common/src/mimetype.rs +++ /dev/null @@ -1,13 +0,0 @@ - -const DEFAULT_BINARY_MIMETYPE : &'static str = "application/octet-stream"; - -pub fn get_mimetype_for_bytes(bytes: &[u8]) -> Option<&'static str> { - infer::get(bytes) - .map(|typ| typ.mime_type()) -} - -pub fn get_mimetype_for_bytes_or_default(bytes: &[u8]) -> &'static str { - infer::get(bytes) - .map(|typ| typ.mime_type()) - .unwrap_or(DEFAULT_BINARY_MIMETYPE) -} diff --git a/crates/lib/media/Cargo.toml b/crates/lib/media/Cargo.toml new file mode 100644 index 0000000000..756b03f4a9 --- /dev/null +++ b/crates/lib/media/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "media" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +name = "media" # NB: Public "media" crate was squatted, so we're good to use this name internally. +path = "src/lib.rs" + +[dependencies] +symphonia = "0.5.1" # Powerful audio decoder and demuxer + +[dev-dependencies] +# None yet diff --git a/crates/lib/media/src/decode_basic_audio_info.rs b/crates/lib/media/src/decode_basic_audio_info.rs new file mode 100644 index 0000000000..798958e1b9 --- /dev/null +++ b/crates/lib/media/src/decode_basic_audio_info.rs @@ -0,0 +1,57 @@ +use std::io::Cursor; +use symphonia::core::formats::FormatOptions; +use symphonia::core::io::{MediaSourceStream, ReadOnlySource}; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; + +pub struct BasicAudioInfo { + pub duration_millis: Option, +} + +pub fn decode_basic_audio_info( + audio_bytes: &[u8], + maybe_mimetype: Option<&str>, + maybe_extension: Option<&str>, +) -> symphonia::core::errors::Result { + + // FIXME(bt, 2022-12-21): This is horribly inefficient. + let bytes = audio_bytes.to_vec(); + let reader = Cursor::new(bytes); + let source = ReadOnlySource::new(reader); + let mss = MediaSourceStream::new(Box::new(source), Default::default()); + + let mut hint = Hint::new(); + if let Some(extension) = maybe_extension { + hint.with_extension(extension); + } + if let Some(mimetype) = maybe_mimetype { + hint.mime_type(mimetype); + } + + // Use the default options for metadata and format readers. + let meta_opts: MetadataOptions = Default::default(); + let fmt_opts: FormatOptions = Default::default(); + + // Probe the media source. + let probed = symphonia::default::get_probe() + .format(&hint, mss, &fmt_opts, &meta_opts)?; + + let format = probed.format; + + // TODO: FIX + let duration = format.default_track() + .map(|track| { + let timebase= track.codec_params.time_base.unwrap(); + let n_frames = track.codec_params.n_frames.unwrap(); + timebase.calc_time(n_frames) + }) + .unwrap(); + + let duration_millis = duration.seconds * 1000; + let frac_millis = (duration.frac * 1000.0).trunc() as u64; + let duration_millis = duration_millis + frac_millis; + + Ok(BasicAudioInfo { + duration_millis: Some(duration_millis), + }) +} \ No newline at end of file diff --git a/crates/lib/media/src/lib.rs b/crates/lib/media/src/lib.rs new file mode 100644 index 0000000000..2e60c385ff --- /dev/null +++ b/crates/lib/media/src/lib.rs @@ -0,0 +1,21 @@ +//! media +//! +//! The purpose of this library is to package audio/video related functions together until they need +//! to be split apart. +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +pub mod decode_basic_audio_info; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index 68d7d2850e..5142088f99 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -20,10 +20,11 @@ config = { path = "../../../lib/config" } container_common = { path = "../../../lib/container_common" } database_queries = { path = "../../../lib/database_queries" } enums = { path = "../../../_types/enums" } -files_common = { path = "../../../lib/files_common" } +files = { path = "../../../lib/files" } google_drive_common = { path = "../../../lib/google_drive_common" } http_server_common = { path = "../../../lib/http_server_common" } jobs_common = { path = "../../../lib/jobs_common" } +media = { path = "../../../lib/media" } memory_caching = { path = "../../../lib/memory_caching" } redis_common = { path = "../../../lib/redis_common" } reusable_types = { path = "../../../_types/reusable_types" } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index 79e30fece2..21cdb36ff3 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -16,8 +16,8 @@ use crate::validations::validate_idempotency_token_format::validate_idempotency_ use database_queries::queries::media_uploads::insert_media_upload::{Args, insert_media_upload}; use enums::core::visibility::Visibility; use enums::files::media_upload_type::MediaUploadType; -use files_common::hash::hash_bytes_sha2; -use files_common::mimetype::{get_mimetype_for_bytes, get_mimetype_for_bytes_or_default}; +use files::hash::hash_bytes_sha2; +use files::mimetype::{get_mimetype_for_bytes, get_mimetype_for_bytes_or_default}; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; @@ -34,6 +34,7 @@ use symphonia::core::formats::FormatOptions; use symphonia::core::io::{MediaSourceStream, ReadOnlySource}; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; +use media::decode_basic_audio_info::decode_basic_audio_info; use tokens::files::media_upload::MediaUploadToken; #[derive(Serialize)] @@ -170,6 +171,7 @@ pub async fn upload_audio_handler( match mimetype { "audio/x-wav" => { // FIXME(bt, 2022-12-21): Ugh. + /* let bytes = bytes.to_vec(); let reader = Cursor::new(bytes); let source = ReadOnlySource::new(reader); @@ -204,8 +206,15 @@ pub async fn upload_audio_handler( let duration_millis = duration.seconds * 1000; let frac_millis = (duration.frac * 1000.0).trunc() as u64; let duration_millis = duration_millis + frac_millis; + */ - maybe_duration_millis = Some(duration_millis); + let basic_info = decode_basic_audio_info(bytes.as_ref(), Some(mimetype), None) + .map_err(|e| { + warn!("file decoding error: {:?}", e); + UploadAudioError::BadInput("could not decode file".to_string()) + })?; + + maybe_duration_millis = basic_info.duration_millis; } _ => {} } From 603a67837820fa24490d6c5faff38aef777417d4 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 05:35:47 -0500 Subject: [PATCH 34/54] new errors crate, cleanup media duration calculation --- Cargo.lock | 8 +++++ Cargo.toml | 1 + crates/lib/errors/Cargo.toml | 20 +++++++++++++ crates/lib/errors/src/lib.rs | 25 ++++++++++++++++ crates/lib/media/Cargo.toml | 5 ++++ .../lib/media/src/decode_basic_audio_info.rs | 30 +++++++++++-------- 6 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 crates/lib/errors/Cargo.toml create mode 100644 crates/lib/errors/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a91df1b9c5..edca6e1420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1742,6 +1742,13 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errors" +version = "0.0.1" +dependencies = [ + "anyhow", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -2860,6 +2867,7 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" name = "media" version = "0.0.1" dependencies = [ + "errors", "symphonia", ] diff --git a/Cargo.toml b/Cargo.toml index 90b196f73e..47e921d6e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "crates/lib/config", "crates/lib/container_common", "crates/lib/database_queries", + "crates/lib/errors", "crates/lib/files", "crates/lib/google_drive_common", "crates/lib/http_server_common", diff --git a/crates/lib/errors/Cargo.toml b/crates/lib/errors/Cargo.toml new file mode 100644 index 0000000000..5993c618d8 --- /dev/null +++ b/crates/lib/errors/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "errors" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +# NB: Public "errors" crate is name-squatted, so we're fine to use this internally. +name = "errors" +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0.40" + +[dev-dependencies] +# None yet diff --git a/crates/lib/errors/src/lib.rs b/crates/lib/errors/src/lib.rs new file mode 100644 index 0000000000..ec5be1b6a3 --- /dev/null +++ b/crates/lib/errors/src/lib.rs @@ -0,0 +1,25 @@ +//! errors +//! +//! The purpose of this library is to pin to a single 'anyhow' and also develop +//! common error utilities. +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +/// Easier to import than anyhow::Result. +pub type AnyhowResult = anyhow::Result; + +/// Useful re-export. +pub use anyhow::anyhow; diff --git a/crates/lib/media/Cargo.toml b/crates/lib/media/Cargo.toml index 756b03f4a9..ed5a46ae05 100644 --- a/crates/lib/media/Cargo.toml +++ b/crates/lib/media/Cargo.toml @@ -13,6 +13,11 @@ name = "media" # NB: Public "media" crate was squatted, so we're good to use thi path = "src/lib.rs" [dependencies] + +# Internal +errors = { path = "../errors" } + +# External symphonia = "0.5.1" # Powerful audio decoder and demuxer [dev-dependencies] diff --git a/crates/lib/media/src/decode_basic_audio_info.rs b/crates/lib/media/src/decode_basic_audio_info.rs index 798958e1b9..d3ef2bd40f 100644 --- a/crates/lib/media/src/decode_basic_audio_info.rs +++ b/crates/lib/media/src/decode_basic_audio_info.rs @@ -1,3 +1,4 @@ +use errors::AnyhowResult; use std::io::Cursor; use symphonia::core::formats::FormatOptions; use symphonia::core::io::{MediaSourceStream, ReadOnlySource}; @@ -12,7 +13,7 @@ pub fn decode_basic_audio_info( audio_bytes: &[u8], maybe_mimetype: Option<&str>, maybe_extension: Option<&str>, -) -> symphonia::core::errors::Result { +) -> AnyhowResult { // FIXME(bt, 2022-12-21): This is horribly inefficient. let bytes = audio_bytes.to_vec(); @@ -38,20 +39,23 @@ pub fn decode_basic_audio_info( let format = probed.format; - // TODO: FIX - let duration = format.default_track() + let maybe_track_duration = format.default_track() .map(|track| { - let timebase= track.codec_params.time_base.unwrap(); - let n_frames = track.codec_params.n_frames.unwrap(); - timebase.calc_time(n_frames) + track.codec_params.time_base + .zip(track.codec_params.n_frames) + .map(|(time_base, n_frames)| { + // NB: This yields the duration of the track + time_base.calc_time(n_frames) + }) }) - .unwrap(); - - let duration_millis = duration.seconds * 1000; - let frac_millis = (duration.frac * 1000.0).trunc() as u64; - let duration_millis = duration_millis + frac_millis; + .flatten() + .map(|time| { + let duration_millis = time.seconds * 1000; + let frac_millis = (time.frac * 1000.0).trunc() as u64; + duration_millis + frac_millis + }); Ok(BasicAudioInfo { - duration_millis: Some(duration_millis), + duration_millis: maybe_track_duration, }) -} \ No newline at end of file +} From f2bfcfeb75bfac6c86e20b3d6272d5aca6e5711e Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 05:49:53 -0500 Subject: [PATCH 35/54] audio codec/encoding --- crates/lib/media/src/decode_basic_audio_info.rs | 10 ++++++++++ .../endpoints/media_uploads/upload_audio.rs | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/lib/media/src/decode_basic_audio_info.rs b/crates/lib/media/src/decode_basic_audio_info.rs index d3ef2bd40f..aa879d3533 100644 --- a/crates/lib/media/src/decode_basic_audio_info.rs +++ b/crates/lib/media/src/decode_basic_audio_info.rs @@ -7,6 +7,7 @@ use symphonia::core::probe::Hint; pub struct BasicAudioInfo { pub duration_millis: Option, + pub codec_name: Option, } pub fn decode_basic_audio_info( @@ -55,7 +56,16 @@ pub fn decode_basic_audio_info( duration_millis + frac_millis }); + let codec_registry = symphonia::default::get_codecs(); + + let maybe_codec_name = format.default_track() + .map(|track| track.codec_params.codec) + .map(|codec_type| codec_registry.get_codec(codec_type)) + .flatten() + .map(|codec_descriptor| codec_descriptor.short_name.to_string()); + Ok(BasicAudioInfo { duration_millis: maybe_track_duration, + codec_name: maybe_codec_name, }) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index 21cdb36ff3..d288201e37 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -166,6 +166,7 @@ pub async fn upload_audio_handler( }; let mut maybe_duration_millis = None; + let mut maybe_codec_name = None; if let Some(mimetype) = maybe_mimetype.as_deref() { match mimetype { @@ -215,6 +216,7 @@ pub async fn upload_audio_handler( })?; maybe_duration_millis = basic_info.duration_millis; + maybe_codec_name = basic_info.codec_name; } _ => {} } @@ -228,7 +230,7 @@ pub async fn upload_audio_handler( original_file_size_bytes: 0, maybe_original_duration_millis: maybe_duration_millis, maybe_original_mime_type: maybe_mimetype, - maybe_original_audio_encoding: None, + maybe_original_audio_encoding: maybe_codec_name.as_deref(), maybe_original_video_encoding: None, maybe_original_frame_width: None, maybe_original_frame_height: None, From 7f51ccd01b933c7dff43dbded7197c04be37c13b Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 21 Dec 2022 05:56:03 -0500 Subject: [PATCH 36/54] file size bytes --- .../src/http_server/endpoints/media_uploads/upload_audio.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index d288201e37..1cb2dd7284 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -165,6 +165,8 @@ pub async fn upload_audio_handler( Some(bytes) => bytes, }; + let file_size_bytes = bytes.len(); + let mut maybe_duration_millis = None; let mut maybe_codec_name = None; @@ -227,7 +229,7 @@ pub async fn upload_audio_handler( uuid_idempotency_token: &uuid_idempotency_token, media_type: MediaUploadType::Audio, maybe_original_filename: upload_media_request.file_name.as_deref(), - original_file_size_bytes: 0, + original_file_size_bytes: file_size_bytes as u64, maybe_original_duration_millis: maybe_duration_millis, maybe_original_mime_type: maybe_mimetype, maybe_original_audio_encoding: maybe_codec_name.as_deref(), From 533cd5e051d043bf41a6a0e0f3aa6cebbba99b90 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 25 Dec 2022 23:49:28 -0500 Subject: [PATCH 37/54] Tests + test data for media uploads --- .idea/dictionaries/bt.xml | 1 + Cargo.lock | 52 ++++++++++++++ .../media_uploads/insert_media_upload.rs | 2 +- crates/lib/media/Cargo.toml | 3 +- .../lib/media/src/decode_basic_audio_info.rs | 66 ++++++++++++++++++ .../endpoints/media_uploads/upload_audio.rs | 44 ++---------- .../audio/flac/zelda_ocarina_small_item.flac | Bin 0 -> 271925 bytes .../audio/m4a/super_mario_bros_lost_life.m4a | Bin 0 -> 369471 bytes ...mario_rpg_beware_the_forests_mushrooms.mp3 | Bin 0 -> 448959 bytes .../ogg/banjo-kazooie_jiggy_appearance.ogg | Bin 0 -> 51163 bytes test_data/audio/wav/sm64_mario_its_me.wav | Bin 0 -> 60600 bytes 11 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 test_data/audio/flac/zelda_ocarina_small_item.flac create mode 100644 test_data/audio/m4a/super_mario_bros_lost_life.m4a create mode 100644 test_data/audio/mp3/super_mario_rpg_beware_the_forests_mushrooms.mp3 create mode 100644 test_data/audio/ogg/banjo-kazooie_jiggy_appearance.ogg create mode 100644 test_data/audio/wav/sm64_mario_its_me.wav diff --git a/.idea/dictionaries/bt.xml b/.idea/dictionaries/bt.xml index 562630d84e..beefee1473 100644 --- a/.idea/dictionaries/bt.xml +++ b/.idea/dictionaries/bt.xml @@ -10,6 +10,7 @@ hifigan scrollback softvc + symphonia tacotron transcoded transcodings diff --git a/Cargo.lock b/Cargo.lock index edca6e1420..95074fe1b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2868,6 +2868,7 @@ name = "media" version = "0.0.1" dependencies = [ "errors", + "log", "symphonia", ] @@ -4722,9 +4723,13 @@ checksum = "17033fe05e4f7f10a6ad602c272bafd2520b2e5cdd9feb61494d9cdce08e002f" dependencies = [ "lazy_static", "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-alac", "symphonia-codec-pcm", "symphonia-codec-vorbis", "symphonia-core", + "symphonia-format-isomp4", "symphonia-format-mkv", "symphonia-format-ogg", "symphonia-format-wav", @@ -4743,6 +4748,40 @@ dependencies = [ "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5d3d53535ae2b7d0e39e82f683cac5398a6c8baca25ff1183e107d13959d3e" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9431b89428c31b01428563df18e52b1aff7c49e71fb80b2fa8e85632094776" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452438d6f32bf07f2f55f35371c3c8e9cce4a028a08b47fb301001f85a950f02" +dependencies = [ + "log", + "symphonia-core", +] + [[package]] name = "symphonia-codec-pcm" version = "0.5.1" @@ -4777,6 +4816,19 @@ dependencies = [ "log", ] +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dce84cea63247dfcc43e12edf2d19406fbb62ebc24bac1b8e6c49f1ba67b890" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + [[package]] name = "symphonia-format-mkv" version = "0.5.1" diff --git a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs index 747ebe8aa3..9145110069 100644 --- a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs +++ b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs @@ -70,7 +70,7 @@ SET args.media_type, args.maybe_original_filename, args.original_file_size_bytes, - args.maybe_original_duration_millis, + args.maybe_original_duration_millis.unwrap_or(0), args.maybe_original_mime_type, args.maybe_original_audio_encoding, args.maybe_original_video_encoding, diff --git a/crates/lib/media/Cargo.toml b/crates/lib/media/Cargo.toml index ed5a46ae05..fc36b15be5 100644 --- a/crates/lib/media/Cargo.toml +++ b/crates/lib/media/Cargo.toml @@ -18,7 +18,8 @@ path = "src/lib.rs" errors = { path = "../errors" } # External -symphonia = "0.5.1" # Powerful audio decoder and demuxer +log = "0.4.14" +symphonia = { version = "0.5.1", features = ["aac", "alac", "mp3", "isomp4"] } # Powerful audio decoder and demuxer [dev-dependencies] # None yet diff --git a/crates/lib/media/src/decode_basic_audio_info.rs b/crates/lib/media/src/decode_basic_audio_info.rs index aa879d3533..2135efd6af 100644 --- a/crates/lib/media/src/decode_basic_audio_info.rs +++ b/crates/lib/media/src/decode_basic_audio_info.rs @@ -69,3 +69,69 @@ pub fn decode_basic_audio_info( codec_name: maybe_codec_name, }) } + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + use errors::AnyhowResult; + use crate::decode_basic_audio_info::decode_basic_audio_info; + + fn test_file(root_file_path: &str) -> PathBuf { + // https://doc.rust-lang.org/cargo/reference/environment-variables.html + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push(format!("../../../{}", root_file_path)); + path + } + + #[test] + fn decode_flac_info() -> AnyhowResult<()> { + let path = test_file("test_data/audio/flac/zelda_ocarina_small_item.flac"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("flac".to_string())); + assert_eq!(info.duration_millis, Some(5120)); + Ok(()) + } + + // NB: Requires symphonia 'aac' and 'isomp4' feature flags + #[test] + fn decode_m4a_alac_info() -> AnyhowResult<()> { + let path = test_file("test_data/audio/m4a/super_mario_bros_lost_life.m4a"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("alac".to_string())); + assert_eq!(info.duration_millis, Some(5493)); + Ok(()) + } + + // NB: Requires symphonia 'mp3' feature flag + #[test] + fn decode_mp3_info() -> AnyhowResult<()> { + let path = test_file("test_data/audio/mp3/super_mario_rpg_beware_the_forests_mushrooms.mp3"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("mp3".to_string())); + assert_eq!(info.duration_millis, Some(15023)); + Ok(()) + } + + #[test] + fn decode_ogg_info() -> AnyhowResult<()> { + let path = test_file("test_data/audio/ogg/banjo-kazooie_jiggy_appearance.ogg"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("vorbis".to_string())); + // assert_eq!(info.duration_millis, Some(5120)); // TODO/FIXME: No duration reported. + Ok(()) + } + + #[test] + fn decode_wav_info_pcm_s16le() -> AnyhowResult<()> { + let path = test_file("test_data/audio/wav/sm64_mario_its_me.wav"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("pcm_s16le".to_string())); + assert_eq!(info.duration_millis, Some(1891)); + Ok(()) + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs index 1cb2dd7284..955d7be1ad 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_audio.rs @@ -172,45 +172,11 @@ pub async fn upload_audio_handler( if let Some(mimetype) = maybe_mimetype.as_deref() { match mimetype { - "audio/x-wav" => { - // FIXME(bt, 2022-12-21): Ugh. - /* - let bytes = bytes.to_vec(); - let reader = Cursor::new(bytes); - let source = ReadOnlySource::new(reader); - let mss = MediaSourceStream::new(Box::new(source), Default::default()); - - // Create a probe hint using the file's extension. [Optional] - let mut hint = Hint::new(); - hint.mime_type(mimetype); - - // Use the default options for metadata and format readers. - let meta_opts: MetadataOptions = Default::default(); - let fmt_opts: FormatOptions = Default::default(); - - // Probe the media source. - let probed = symphonia::default::get_probe() - .format(&hint, mss, &fmt_opts, &meta_opts) - .map_err(|e| { - warn!("Invalid format: {:?}", e); - UploadAudioError::BadInput("bad file".to_string()) - })?; - - let format = probed.format; - - let duration = format.default_track() - .map(|track| { - let timebase= track.codec_params.time_base.unwrap(); - let n_frames = track.codec_params.n_frames.unwrap(); - timebase.calc_time(n_frames) - }) - .unwrap(); - - let duration_millis = duration.seconds * 1000; - let frac_millis = (duration.frac * 1000.0).trunc() as u64; - let duration_millis = duration_millis + frac_millis; - */ - + "video/x-msvideo" => { + // .avi + }, + "audio/x-wav" | "audio/ogg" => { + // .wav, .ogg let basic_info = decode_basic_audio_info(bytes.as_ref(), Some(mimetype), None) .map_err(|e| { warn!("file decoding error: {:?}", e); diff --git a/test_data/audio/flac/zelda_ocarina_small_item.flac b/test_data/audio/flac/zelda_ocarina_small_item.flac new file mode 100644 index 0000000000000000000000000000000000000000..87885640cdba1e503f7e2c90d5f17320a2379eee GIT binary patch literal 271925 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

9;2hToh*f)?c2dfMA+}pHlA4z1!}-V5xsi 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