diff --git a/.sqlx/query-2a19cc533815aa3325b940eafe56b9f014fedb3b360b954935d6ec13ff14302d.json b/.sqlx/query-2a19cc533815aa3325b940eafe56b9f014fedb3b360b954935d6ec13ff14302d.json new file mode 100644 index 0000000000..d437747d00 --- /dev/null +++ b/.sqlx/query-2a19cc533815aa3325b940eafe56b9f014fedb3b360b954935d6ec13ff14302d.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\nINSERT INTO characters\nSET\n token = ?,\n character_type = ?,\n is_active = false,\n character_name = ?,\n maybe_description = ?,\n maybe_avatar_media_token = NULL,\n maybe_full_image_media_token = NULL,\n maybe_original_upload_media_token = ?,\n maybe_creator_user_token = ?,\n creator_ip_address = ?,\n kinovi_character_id = ?,\n kinovi_character_name = ?,\n maybe_kinovi_asset_id = NULL,\n maybe_generic_inference_job_token = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "2a19cc533815aa3325b940eafe56b9f014fedb3b360b954935d6ec13ff14302d" +} diff --git a/.sqlx/query-8c130ebd0d192355844863b3ebd44952aa8715eebe96375fb28f9f12baf7f9b1.json b/.sqlx/query-8c130ebd0d192355844863b3ebd44952aa8715eebe96375fb28f9f12baf7f9b1.json new file mode 100644 index 0000000000..e3863d3510 --- /dev/null +++ b/.sqlx/query-8c130ebd0d192355844863b3ebd44952aa8715eebe96375fb28f9f12baf7f9b1.json @@ -0,0 +1,157 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n id as `id: u64`,\n token as `token: tokens::tokens::characters::CharacterToken`,\n character_type as `character_type: enums::by_table::characters::character_type::CharacterType`,\n is_active as `is_active: bool`,\n character_name,\n maybe_description,\n maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n kinovi_character_id,\n kinovi_character_name,\n maybe_kinovi_asset_id\nFROM characters\nWHERE maybe_creator_user_token = ?\n AND is_active = true\n AND deleted_at IS NULL\n AND id < ?\nORDER BY id DESC\nLIMIT ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: u64", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "token: tokens::tokens::characters::CharacterToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "character_type: enums::by_table::characters::character_type::CharacterType", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 96 + } + }, + { + "ordinal": 3, + "name": "is_active: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL | MULTIPLE_KEY", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 4, + "name": "character_name", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "maybe_description", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 2048 + } + }, + { + "ordinal": 6, + "name": "maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 7, + "name": "maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 8, + "name": "maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 9, + "name": "maybe_creator_user_token: tokens::tokens::users::UserToken", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 10, + "name": "kinovi_character_id", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 256 + } + }, + { + "ordinal": 11, + "name": "kinovi_character_name", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 12, + "name": "maybe_kinovi_asset_id", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 256 + } + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true + ] + }, + "hash": "8c130ebd0d192355844863b3ebd44952aa8715eebe96375fb28f9f12baf7f9b1" +} diff --git a/.sqlx/query-b0ba0fe63908df3d66b4820ca47cbb773786e480464c84a278fe63b1fa8913b4.json b/.sqlx/query-b0ba0fe63908df3d66b4820ca47cbb773786e480464c84a278fe63b1fa8913b4.json new file mode 100644 index 0000000000..0965177d50 --- /dev/null +++ b/.sqlx/query-b0ba0fe63908df3d66b4820ca47cbb773786e480464c84a278fe63b1fa8913b4.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = NULL,\n\n maybe_wallet_ledger_entry_token = NULL,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = NULL,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "b0ba0fe63908df3d66b4820ca47cbb773786e480464c84a278fe63b1fa8913b4" +} diff --git a/.sqlx/query-e9078c593087df3da20dbd193cdad102d6bb3ae83ee81c8c4ad75fe184e03c92.json b/.sqlx/query-e9078c593087df3da20dbd193cdad102d6bb3ae83ee81c8c4ad75fe184e03c92.json new file mode 100644 index 0000000000..a0edd42885 --- /dev/null +++ b/.sqlx/query-e9078c593087df3da20dbd193cdad102d6bb3ae83ee81c8c4ad75fe184e03c92.json @@ -0,0 +1,146 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n token as `token: tokens::tokens::characters::CharacterToken`,\n character_type as `character_type: enums::by_table::characters::character_type::CharacterType`,\n is_active as `is_active: bool`,\n character_name,\n maybe_description,\n maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n kinovi_character_id,\n kinovi_character_name,\n maybe_kinovi_asset_id\nFROM characters\nWHERE token = ?\n AND deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token: tokens::tokens::characters::CharacterToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 1, + "name": "character_type: enums::by_table::characters::character_type::CharacterType", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 96 + } + }, + { + "ordinal": 2, + "name": "is_active: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL | MULTIPLE_KEY", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 3, + "name": "character_name", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 4, + "name": "maybe_description", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 2048 + } + }, + { + "ordinal": 5, + "name": "maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 6, + "name": "maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 7, + "name": "maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 8, + "name": "maybe_creator_user_token: tokens::tokens::users::UserToken", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 9, + "name": "kinovi_character_id", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 256 + } + }, + { + "ordinal": 10, + "name": "kinovi_character_name", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 11, + "name": "maybe_kinovi_asset_id", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 256 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true + ] + }, + "hash": "e9078c593087df3da20dbd193cdad102d6bb3ae83ee81c8c4ad75fe184e03c92" +} diff --git a/.sqlx/query-eae0b979383989f7a9af8ddd678ab0c0403952b8b98d9fcb078ae401ae7fa4e3.json b/.sqlx/query-eae0b979383989f7a9af8ddd678ab0c0403952b8b98d9fcb078ae401ae7fa4e3.json new file mode 100644 index 0000000000..3d6559ec05 --- /dev/null +++ b/.sqlx/query-eae0b979383989f7a9af8ddd678ab0c0403952b8b98d9fcb078ae401ae7fa4e3.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\nUPDATE characters\nSET\n character_name = ?,\n kinovi_character_name = ?,\n maybe_description = ?\nWHERE token = ?\n AND deleted_at IS NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "eae0b979383989f7a9af8ddd678ab0c0403952b8b98d9fcb078ae401ae7fa4e3" +} diff --git a/_database/sql/migrations/2026-04-02-083548-0000_characters/down.sql b/_database/sql/migrations/2026-04-02-083548-0000_characters/down.sql new file mode 100644 index 0000000000..91f03efa2b --- /dev/null +++ b/_database/sql/migrations/2026-04-02-083548-0000_characters/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE IF EXISTS characters; diff --git a/_database/sql/migrations/2026-04-02-083548-0000_characters/up.sql b/_database/sql/migrations/2026-04-02-083548-0000_characters/up.sql new file mode 100644 index 0000000000..e348f4bcbe --- /dev/null +++ b/_database/sql/migrations/2026-04-02-083548-0000_characters/up.sql @@ -0,0 +1,101 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Characters are reusable entities that can be referenced in video/image generations. +-- A character is created from one or more reference images and registered with third-party +-- systems (e.g. Kinovi/Seedance) that support character-based generation. +CREATE TABLE characters ( + -- Not used for anything except replication (private, not even used in queries) + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Public-facing "primary key" (PUBLIC) + token VARCHAR(32) NOT NULL, + + -- The type or "features" of the character. + character_type VARCHAR(24) NOT NULL, + + -- Whether the character has been fully created and is available for use. + -- A character may exist in the database before its creation job completes. + is_active BOOLEAN NOT NULL DEFAULT false, + + -- ========== CHARACTER DETAILS ========== + + -- User's display name for the character. + character_name VARCHAR(255) DEFAULT NULL, + + -- Optional user description of the character. + maybe_description VARCHAR(512) DEFAULT NULL, + + -- ========== MEDIA ========== + + -- Avatar image (cropped to face, ideally). + maybe_avatar_media_token VARCHAR(32) DEFAULT NULL, + + -- Full-size reference image. + maybe_full_image_media_token VARCHAR(32) DEFAULT NULL, + + -- TEMPORARY FOR GOING FAST : + -- WE'LL NEED TO BREAK THIS INTO ANOTHER TABLE IF WE WANT TO SUPPORT MULTIPLE IMAGES. + -- Image the character was originally created with. + maybe_original_upload_media_token VARCHAR(32) DEFAULT NULL, + + -- ========== CREATOR ========== + + -- The user who created this character. + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + creator_ip_address VARCHAR(40) NOT NULL, + + -- ========== KINOVI (TEMPORARY) ========== + + -- TEMPORARY FOR GOING FAST : + -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- The identifier in the third-party system. + -- e.g. Kinovi's "char_1774752056469_2wlxoq" + kinovi_character_id VARCHAR(64) DEFAULT NULL, + + -- TEMPORARY FOR GOING FAST : + -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- The name used by the third party. + -- Important for Kinovi since characters are referenced by name in prompts. + kinovi_character_name VARCHAR(255) DEFAULT NULL, + + -- TEMPORARY FOR GOING FAST : + -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- This might be the upstream third party of kinovi. + -- e.g. Kinovi's assetId "asset-20260329104101-kk5kl" + maybe_kinovi_asset_id VARCHAR(64) DEFAULT NULL, + + -- TEMPORARY FOR GOING FAST : + -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- Our job system token (if the character was created via a job). + maybe_generic_inference_job_token VARCHAR(32) DEFAULT NULL, + + -- Maybe this doesn't exist + -- -- TEMPORARY FOR GOING FAST : + -- -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- -- Third party's job system token (if any). + -- maybe_kinovi_job_id VARCHAR(64) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP DEFAULT NULL, + + -- ========== INDICES ========== + + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY index_character_type (character_type), + KEY index_maybe_creator_user_token (maybe_creator_user_token), + KEY index_maybe_creator_user_token_active (maybe_creator_user_token, is_active), + KEY index_is_active (is_active), + KEY index_kinovi_character_id (kinovi_character_id), + KEY index_maybe_generic_inference_job_token (maybe_generic_inference_job_token), + KEY index_created_at (created_at), + KEY index_deleted_at (deleted_at) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_database/sql/migrations_squashed/characters/characters.sql b/_database/sql/migrations_squashed/characters/characters.sql index b78132d2fc..af5d4bc6e9 100644 --- a/_database/sql/migrations_squashed/characters/characters.sql +++ b/_database/sql/migrations_squashed/characters/characters.sql @@ -15,6 +15,9 @@ CREATE TABLE characters ( -- Effective "primary key" (PUBLIC) token VARCHAR(32) NOT NULL, + -- The type or "features" of the character. + character_type VARCHAR(24) NOT NULL, + -- Whether the character has been fully created and is available for use. -- A character may exist in the database before its creation job completes. is_active BOOLEAN NOT NULL DEFAULT false, @@ -22,7 +25,7 @@ CREATE TABLE characters ( -- ========== CHARACTER DETAILS ========== -- Display name for the character. - name VARCHAR(255) NOT NULL, + character_name VARCHAR(255) DEFAULT NULL, -- Optional description of the character. maybe_description VARCHAR(512) DEFAULT NULL, @@ -35,6 +38,11 @@ CREATE TABLE characters ( -- Full-size reference image. maybe_full_image_media_token VARCHAR(32) DEFAULT NULL, + -- TEMPORARY FOR GOING FAST : + -- WE'LL NEED TO BREAK THIS INTO ANOTHER TABLE IF WE WANT TO SUPPORT MULTIPLE IMAGES. + -- Image the character was originally created with. + maybe_original_upload_media_token VARCHAR(32) DEFAULT NULL, + -- ========== CREATOR ========== -- The user who created this character. @@ -43,6 +51,37 @@ CREATE TABLE characters ( -- For abuse tracking. creator_ip_address VARCHAR(40) NOT NULL, + -- ========== KINOVI (TEMPORARY) ========== + + -- TEMPORARY FOR GOING FAST : + -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- The identifier in the third-party system. + -- e.g. Kinovi's "char_1774752056469_2wlxoq" + kinovi_character_id VARCHAR(64) DEFAULT NULL, + + -- TEMPORARY FOR GOING FAST : + -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- The name used by the third party. + -- Important for Kinovi since characters are referenced by name in prompts. + kinovi_character_name VARCHAR(255) DEFAULT NULL, + + -- TEMPORARY FOR GOING FAST : + -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- This might be the upstream third party of kinovi. + -- e.g. Kinovi's assetId "asset-20260329104101-kk5kl" + maybe_kinovi_asset_id VARCHAR(64) DEFAULT NULL, + + -- TEMPORARY FOR GOING FAST : + -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- Our job system token (if the character was created via a job). + maybe_generic_inference_job_token VARCHAR(32) DEFAULT NULL, + + -- Maybe this doesn't exist + -- -- TEMPORARY FOR GOING FAST : + -- -- IDEALLY WE MOVE THIS TO A SEPARATE TABLE. + -- -- Third party's job system token (if any). + -- maybe_kinovi_job_id VARCHAR(64) DEFAULT NULL, + -- ========== TIMESTAMPS ========== created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -53,8 +92,13 @@ CREATE TABLE characters ( PRIMARY KEY (id), UNIQUE KEY (token), + KEY index_character_type (character_type), KEY index_maybe_creator_user_token (maybe_creator_user_token), + KEY index_maybe_creator_user_token_active (maybe_creator_user_token, is_active), KEY index_is_active (is_active), - KEY index_created_at (created_at) + KEY index_kinovi_character_id (kinovi_character_id), + KEY index_maybe_generic_inference_job_token (maybe_generic_inference_job_token), + KEY index_created_at (created_at), + KEY index_deleted_at (deleted_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_tools/postman/collections/ArtCraft API/characters/.resources/definition.yaml b/_tools/postman/collections/ArtCraft API/characters/.resources/definition.yaml new file mode 100644 index 0000000000..ab13db1a68 --- /dev/null +++ b/_tools/postman/collections/ArtCraft API/characters/.resources/definition.yaml @@ -0,0 +1,3 @@ +$kind: collection +name: characters +order: 4767 diff --git a/_tools/postman/collections/ArtCraft API/characters/Create Character.request.yaml b/_tools/postman/collections/ArtCraft API/characters/Create Character.request.yaml new file mode 100644 index 0000000000..8996dc3e33 --- /dev/null +++ b/_tools/postman/collections/ArtCraft API/characters/Create Character.request.yaml @@ -0,0 +1,16 @@ +$kind: http-request +name: Create Character +url: "{{api_host}}/v1/character/create" +method: POST +body: + type: json + content: >- + { + "uuid_idempotency_token": "{{$randomUUID}}", + "model": "seedance_2p0", + //"image_media_token": "m_v2hjh936k72knmggmqqa2xfs9sge8k", // steampunk clown + //"image_media_token": "m_13m13fm33bw3cxvfrncyc6873z0dyy", // mochi on couch + "image_media_token": "m_cp20gcxsattt6j7699brs4ndqf803z", // t-rex + "character_name": "T-rex" + } +order: 1775175681198 diff --git a/_tools/postman/collections/ArtCraft API/characters/Delete Character.request.yaml b/_tools/postman/collections/ArtCraft API/characters/Delete Character.request.yaml new file mode 100644 index 0000000000..5264852e75 --- /dev/null +++ b/_tools/postman/collections/ArtCraft API/characters/Delete Character.request.yaml @@ -0,0 +1,5 @@ +$kind: http-request +name: Delete Character +url: "{{api_host}}/v1/character/character_c40dzeq9pa1qtfy19q9f1w" +method: DELETE +order: 76446840865701 diff --git a/_tools/postman/collections/ArtCraft API/characters/Edit Character.request.yaml b/_tools/postman/collections/ArtCraft API/characters/Edit Character.request.yaml new file mode 100644 index 0000000000..6ba1f34ee6 --- /dev/null +++ b/_tools/postman/collections/ArtCraft API/characters/Edit Character.request.yaml @@ -0,0 +1,18 @@ +$kind: http-request +name: Edit Character +url: "{{api_host}}/v1/character/edit" +method: POST +body: + type: json + content: |- + { + /// The character to edit. + "token": "character_4aj66rcqsbjmv3jp5ek4fc", + + /// Updated name (if changing). + "updated_name": "Mochi", + + /// Updated description (if changing). + "updated_description": "New Description" + } +order: 487436956121943 diff --git a/_tools/postman/collections/ArtCraft API/generate/video/multi_function/Seedance 2.0 (character ref to video).request.yaml b/_tools/postman/collections/ArtCraft API/generate/video/multi_function/Seedance 2.0 (character ref to video).request.yaml new file mode 100644 index 0000000000..aebddb2a9c --- /dev/null +++ b/_tools/postman/collections/ArtCraft API/generate/video/multi_function/Seedance 2.0 (character ref to video).request.yaml @@ -0,0 +1,23 @@ +$kind: http-request +name: Seedance 2.0 (character ref to video) +url: "{{api_host}}/v1/generate/video/multi_function/seedance_2p0" +method: POST +headers: + - key: Accept + value: application/json + - key: Session + value: "" + disabled: true + - key: Session + value: "" + disabled: true +body: + type: json + content: > + { + "uuid_idempotency_token": "{{$randomUUID}}", + "prompt": "@Mochi is riding on the shoulder of @Steampunk Clown while they're walking through a crowded shopping mall in the sci-fi future. Retro 80's aesthetics while simultaneously futuristic and steam punk. @Mochi licks her lips and barks.", + "duration_seconds": 5, + "reference_character_tokens": ["character_beph3xd1z2m4kkbedra24f", "character_4aj66rcqsbjmv3jp5ek4fc"] + } +order: 3802 diff --git a/_tools/postman/collections/ArtCraft API/users/Google Account Create.request.yaml b/_tools/postman/collections/ArtCraft API/users/Google Account Create.request.yaml index 9dd75fd55c..717917cb4d 100644 --- a/_tools/postman/collections/ArtCraft API/users/Google Account Create.request.yaml +++ b/_tools/postman/collections/ArtCraft API/users/Google Account Create.request.yaml @@ -8,6 +8,6 @@ body: type: json content: |- { - "google_credential": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyN...[redacted]" + "google_credential": "...[redacted]" } order: 4000 diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/characters/create_character.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/create_character.rs index 92b9a768d9..bfec60e703 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/characters/create_character.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/create_character.rs @@ -16,6 +16,12 @@ pub struct CreateCharacterRequest { /// The reference image media file token. pub image_media_token: MediaFileToken, + + /// Name of the character. + pub character_name: String, + + /// Description of the character. + pub character_description: Option, } /// Response body for creating a character. diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs index 533af8f3ad..d155b36239 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use tokens::tokens::characters::CharacterToken; use tokens::tokens::generic_inference_jobs::InferenceJobToken; use tokens::tokens::media_files::MediaFileToken; use utoipa::ToSchema; @@ -29,6 +30,10 @@ pub struct Seedance2p0MultiFunctionVideoGenRequest { /// Optional reference audio files. pub reference_audio_media_tokens: Option>, + /// Optional character tokens to reference in the prompt. + /// Characters are referenced in prompts as @CharacterName. + pub reference_character_tokens: Option>, + /// Video aspect ratio / resolution. pub aspect_ratio: Option, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_seedance2p0.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_seedance2p0.rs index 35594e1522..4a539ee772 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_seedance2p0.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_seedance2p0.rs @@ -34,6 +34,7 @@ pub (crate) fn estimate_video_cost_artcraft_seedance2p0( reference_image_urls: None, reference_video_urls: None, reference_audio_urls: None, + character_ids: None, use_face_blur_hack: None, host_override: None, }; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0.rs index 502fe01074..29e2fbc6ee 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0.rs @@ -22,6 +22,7 @@ pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0( reference_image_urls: None, reference_video_urls: None, reference_audio_urls: None, + character_ids: None, use_face_blur_hack: None, host_override: None, }; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_seedance2p0.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_seedance2p0.rs index 18b7c6054c..849d828251 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_seedance2p0.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_seedance2p0.rs @@ -23,6 +23,7 @@ pub async fn execute_artcraft_seedance2p0( aspect_ratio: plan.aspect_ratio, duration_seconds: plan.duration_seconds, batch_count: Some(plan.batch_count), + reference_character_tokens: None, }; let response = seedance_2p0_multi_function_video_gen( diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs index 932a342329..fe72bfe644 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs @@ -44,6 +44,7 @@ pub async fn execute_seedance2pro_seedance2p0( reference_image_urls, reference_video_urls, reference_audio_urls, + character_ids: None, use_face_blur_hack: None, host_override: None, }; diff --git a/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs b/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs index dcc9365c54..88d9c3ddf9 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs @@ -49,6 +49,10 @@ pub struct GenerateVideoArgs<'a> { /// Sent in a separate `audioUrls` field (not in `uploadedUrls`). pub reference_audio_urls: Option>, + /// Optional Kinovi character IDs to reference in the prompt. + /// Characters are referenced in prompts as @CharacterName. + pub character_ids: Option>, + /// Controls the `faceBlurMode` field: true sends "on", false sends "off", None omits it. pub use_face_blur_hack: Option, @@ -69,6 +73,7 @@ impl std::fmt::Debug for GenerateVideoArgs<'_> { .field("reference_image_urls", &self.reference_image_urls) .field("reference_video_urls", &self.reference_video_urls) .field("reference_audio_urls", &self.reference_audio_urls) + .field("character_ids", &self.character_ids) .field("use_face_blur_hack", &self.use_face_blur_hack) .field("host_override", &self.host_override) .finish() @@ -261,6 +266,7 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result AnyhowResult<()> { + setup_test_logging(LevelFilter::Trace); + let session = test_session()?; + let args = GenerateVideoArgs { + session: &session, + model_type: ModelType::Seedance2Pro, + prompt: "@Steampunk Clown is juggling flaming torches in a circus tent.".to_string(), + resolution: Resolution::Landscape16x9, + duration_seconds: 5, + batch_count: BatchCount::One, + start_frame_url: None, + end_frame_url: None, + reference_image_urls: None, + reference_video_urls: None, + reference_audio_urls: None, + character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string()]), + use_face_blur_hack: None, + host_override: None, + }; + let result = generate_video(args).await?; + println!("Task ID: {}", result.task_id); + println!("Order ID: {}", result.order_id); + assert!(!result.task_id.is_empty()); + assert!(!result.order_id.is_empty()); + assert_eq!(1, 2); // NB: Intentional failure to inspect output. + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real cookies, costs money + async fn test_text_prompt_with_character_fast() -> AnyhowResult<()> { + setup_test_logging(LevelFilter::Trace); + let session = test_session()?; + let args = GenerateVideoArgs { + session: &session, + model_type: ModelType::Seedance2Fast, + prompt: "@Mochi the female shiba inu is eating a cheese pizza while standing on the table".to_string(), + resolution: Resolution::Portrait9x16, + duration_seconds: 5, + batch_count: BatchCount::One, + start_frame_url: None, + end_frame_url: None, + reference_image_urls: None, + reference_video_urls: None, + reference_audio_urls: None, + character_ids: Some(vec![MOCHI_ID.to_string()]), + use_face_blur_hack: None, + host_override: None, + }; + let result = generate_video(args).await?; + println!("Task ID: {}", result.task_id); + println!("Order ID: {}", result.order_id); + assert!(!result.task_id.is_empty()); + assert!(!result.order_id.is_empty()); + assert_eq!(1, 2); // NB: Intentional failure to inspect output. + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real cookies, costs money + async fn test_character_with_image_ref_pro() -> AnyhowResult<()> { + setup_test_logging(LevelFilter::Trace); + let session = test_session()?; + let args = GenerateVideoArgs { + session: &session, + model_type: ModelType::Seedance2Pro, + prompt: "@Steampunk Clown is walking up to pet a dog on the couch.".to_string(), + resolution: Resolution::Landscape16x9, + duration_seconds: 5, + batch_count: BatchCount::One, + start_frame_url: None, + end_frame_url: None, + reference_image_urls: Some(vec![ + "https://static.seedance2-pro.com/materials/20260329/1774752385699-1ff44886.jpeg".to_string(), + ]), + reference_video_urls: None, + reference_audio_urls: None, + character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string()]), + use_face_blur_hack: None, + host_override: None, + }; + let result = generate_video(args).await?; + println!("Task ID: {}", result.task_id); + println!("Order ID: {}", result.order_id); + assert!(!result.task_id.is_empty()); + assert!(!result.order_id.is_empty()); + assert_eq!(1, 2); // NB: Intentional failure to inspect output. + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real cookies, costs money + async fn test_two_characters_fast() -> AnyhowResult<()> { + setup_test_logging(LevelFilter::Trace); + let session = test_session()?; + let args = GenerateVideoArgs { + session: &session, + model_type: ModelType::Seedance2Fast, + prompt: "@Steampunk Clown and @Mochi are playing fetch in a sunny park.".to_string(), + resolution: Resolution::Landscape16x9, + duration_seconds: 5, + batch_count: BatchCount::One, + start_frame_url: None, + end_frame_url: None, + reference_image_urls: None, + reference_video_urls: None, + reference_audio_urls: None, + character_ids: Some(vec![ + STEAMPUNK_CLOWN_ID.to_string(), + MOCHI_ID.to_string(), + ]), + use_face_blur_hack: None, + host_override: None, + }; + let result = generate_video(args).await?; + println!("Task ID: {}", result.task_id); + println!("Order ID: {}", result.order_id); + assert!(!result.task_id.is_empty()); + assert!(!result.order_id.is_empty()); + assert_eq!(1, 2); // NB: Intentional failure to inspect output. + Ok(()) + } + } } } diff --git a/crates/api_clients/seedance2pro_client/src/requests/generate_video/request_types.rs b/crates/api_clients/seedance2pro_client/src/requests/generate_video/request_types.rs index 030cc8274f..34a0e3523e 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/generate_video/request_types.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/generate_video/request_types.rs @@ -30,6 +30,8 @@ pub (super) struct ApiParams { pub mode: &'static str, #[serde(rename = "faceBlurMode", skip_serializing_if = "Option::is_none")] pub face_blur_mode: Option<&'static str>, + #[serde(rename = "characterIds", skip_serializing_if = "Option::is_none")] + pub character_ids: Option>, #[serde(rename = "uploadedUrls", skip_serializing_if = "Option::is_none")] pub uploaded_urls: Option>, #[serde(rename = "audioUrls", skip_serializing_if = "Option::is_none")] diff --git a/crates/api_clients/seedance2pro_client/src/requests/mod.rs b/crates/api_clients/seedance2pro_client/src/requests/mod.rs index 3255083b76..767c04fc68 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/mod.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/mod.rs @@ -4,4 +4,5 @@ pub mod kinovi_host; pub mod poll_characters; pub mod poll_orders; pub mod prepare_file_upload; +pub mod update_character; pub mod upload_file; diff --git a/crates/api_clients/seedance2pro_client/src/requests/update_character/mod.rs b/crates/api_clients/seedance2pro_client/src/requests/update_character/mod.rs new file mode 100644 index 0000000000..e7abf05796 --- /dev/null +++ b/crates/api_clients/seedance2pro_client/src/requests/update_character/mod.rs @@ -0,0 +1,2 @@ +pub mod update_character; +pub(crate) mod request_types; diff --git a/crates/api_clients/seedance2pro_client/src/requests/update_character/request_types.rs b/crates/api_clients/seedance2pro_client/src/requests/update_character/request_types.rs new file mode 100644 index 0000000000..963523f3aa --- /dev/null +++ b/crates/api_clients/seedance2pro_client/src/requests/update_character/request_types.rs @@ -0,0 +1,44 @@ +use serde_derive::{Deserialize, Serialize}; + +// ======================== Request ======================== + +#[derive(Serialize, Debug)] +pub(super) struct BatchRequest { + #[serde(rename = "0")] + pub zero: BatchRequestInner, +} + +#[derive(Serialize, Debug)] +pub(super) struct BatchRequestInner { + pub json: BatchRequestJson, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(super) struct BatchRequestJson { + pub character_id: String, + pub name: String, + pub description: String, +} + +// ======================== Response ======================== + +#[derive(Deserialize, Debug)] +pub(super) struct BatchResponseItem { + pub result: BatchResponseResult, +} + +#[derive(Deserialize, Debug)] +pub(super) struct BatchResponseResult { + pub data: BatchResponseData, +} + +#[derive(Deserialize, Debug)] +pub(super) struct BatchResponseData { + pub json: UpdateCharacterResponseJson, +} + +#[derive(Deserialize, Debug)] +pub(super) struct UpdateCharacterResponseJson { + pub success: bool, +} diff --git a/crates/api_clients/seedance2pro_client/src/requests/update_character/update_character.rs b/crates/api_clients/seedance2pro_client/src/requests/update_character/update_character.rs new file mode 100644 index 0000000000..c9ca66b658 --- /dev/null +++ b/crates/api_clients/seedance2pro_client/src/requests/update_character/update_character.rs @@ -0,0 +1,203 @@ +use crate::creds::seedance2pro_session::Seedance2ProSession; +use crate::error::seedance2pro_client_error::Seedance2ProClientError; +use crate::error::seedance2pro_error::Seedance2ProError; +use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError; +use crate::requests::update_character::request_types::*; +use crate::requests::kinovi_host::{KinoviHost, resolve_host}; +use crate::utils::common_headers::FIREFOX_USER_AGENT; +use log::info; +use wreq::Client; +use wreq_util::Emulation; + +// --- Public args --- + +pub struct UpdateCharacterArgs<'a> { + pub session: &'a Seedance2ProSession, + + /// The Kinovi character ID (e.g. "char_1775184656440_vsy3eu"). + pub character_id: String, + + /// The new name for the character. + pub name: String, + + /// The new description (empty string clears it). + pub description: String, + + /// Override the default host (kinovi.ai). + pub host_override: Option, +} + +// --- Public response --- + +#[derive(Debug)] +pub struct UpdateCharacterResponse { + pub success: bool, +} + +// --- Implementation --- + +pub async fn update_character(args: UpdateCharacterArgs<'_>) -> Result { + let host = resolve_host(args.host_override.as_ref()); + let base_url = host.api_base_url(); + let url = format!("{}/api/trpc/character.updateCharacter?batch=1", base_url); + + info!("Updating character '{}' (name='{}', description='{}')", args.character_id, args.name, args.description); + + let request_body = BatchRequest { + zero: BatchRequestInner { + json: BatchRequestJson { + character_id: args.character_id.clone(), + name: args.name, + description: args.description, + }, + }, + }; + + let cookie = args.session.cookies.as_str(); + let referer = format!("{}/app/characters", base_url); + + let client = Client::builder() + .emulation(Emulation::Firefox143) + .build() + .map_err(|err| Seedance2ProClientError::WreqClientError(err))?; + + let response = client.post(&url) + .header("User-Agent", FIREFOX_USER_AGENT) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.9") + .header("Accept-Encoding", "gzip, deflate, br, zstd") + .header("Referer", &referer) + .header("Content-Type", "application/json") + .header("x-trpc-source", "client") + .header("Origin", base_url) + .header("Connection", "keep-alive") + .header("Cookie", cookie) + .header("Sec-Fetch-Dest", "empty") + .header("Sec-Fetch-Mode", "cors") + .header("Sec-Fetch-Site", "same-origin") + .header("Priority", "u=4") + .header("TE", "trailers") + .json(&request_body) + .send() + .await + .map_err(|err| Seedance2ProGenericApiError::WreqError(err))?; + + let status = response.status(); + let response_body = response.text() + .await + .map_err(|err| Seedance2ProGenericApiError::WreqError(err))?; + + info!("Update character response: status={}, body={}", status, response_body); + + if !status.is_success() { + return Err(Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { + status_code: status, + body: response_body, + }.into()); + } + + let batch_response: Vec = serde_json::from_str(&response_body) + .map_err(|err| Seedance2ProGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.clone()))?; + + let data = batch_response + .into_iter() + .next() + .ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse( + "Empty batch response array".to_string() + ))? + .result + .data + .json; + + Ok(UpdateCharacterResponse { + success: data.success, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::seedance2pro_session::Seedance2ProSession; + use crate::test_utils::get_test_cookies::get_test_cookies; + use crate::test_utils::setup_test_logging::setup_test_logging; + use errors::AnyhowResult; + use log::LevelFilter; + + fn test_session() -> AnyhowResult { + let cookies = get_test_cookies()?; + Ok(Seedance2ProSession::from_cookies_string(cookies)) + } + + // Steampunk Clown character + const TEST_CHARACTER_ID: &str = "char_1775176566518_sik0te"; + + #[tokio::test] + #[ignore] // manually test — requires real cookies, modifies real data + async fn test_rename_character() -> AnyhowResult<()> { + setup_test_logging(LevelFilter::Trace); + let session = test_session()?; + + let result = update_character(UpdateCharacterArgs { + session: &session, + character_id: TEST_CHARACTER_ID.to_string(), + name: "Steampunk Clown (renamed)".to_string(), + description: "A steampunk clown character for testing".to_string(), + host_override: None, + }).await?; + + println!("Success: {}", result.success); + assert!(result.success); + assert_eq!(1, 2); // NB: Intentional failure to inspect output. + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real cookies, modifies real data + async fn test_rename_character_clear_description() -> AnyhowResult<()> { + setup_test_logging(LevelFilter::Trace); + let session = test_session()?; + + let result = update_character(UpdateCharacterArgs { + session: &session, + character_id: TEST_CHARACTER_ID.to_string(), + name: "Steampunk Clown".to_string(), + description: "".to_string(), + host_override: None, + }).await?; + + println!("Success: {}", result.success); + assert!(result.success); + assert_eq!(1, 2); // NB: Intentional failure to inspect output. + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real cookies, modifies real data + async fn test_rename_and_restore() -> AnyhowResult<()> { + setup_test_logging(LevelFilter::Trace); + let session = test_session()?; + + // Rename + let result = update_character(UpdateCharacterArgs { + session: &session, + character_id: TEST_CHARACTER_ID.to_string(), + name: "Temporary Name".to_string(), + description: "Temporary description".to_string(), + host_override: None, + }).await?; + assert!(result.success); + + // Restore + let result = update_character(UpdateCharacterArgs { + session: &session, + character_id: TEST_CHARACTER_ID.to_string(), + name: "Steampunk Clown".to_string(), + description: "A steampunk clown".to_string(), + host_override: None, + }).await?; + assert!(result.success); + + assert_eq!(1, 2); // NB: Intentional failure to inspect output. + Ok(()) + } +} diff --git a/crates/schema/database/mysql_queries/src/queries/characters/batch_lookup_characters_by_token_for_prompting.rs b/crates/schema/database/mysql_queries/src/queries/characters/batch_lookup_characters_by_token_for_prompting.rs new file mode 100644 index 0000000000..5b71adf337 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/characters/batch_lookup_characters_by_token_for_prompting.rs @@ -0,0 +1,54 @@ +use sqlx::pool::PoolConnection; +use sqlx::{FromRow, MySql, QueryBuilder}; + +use tokens::tokens::characters::CharacterToken; + +/// Character data needed for constructing prompts with character references. +#[derive(Debug, FromRow)] +pub struct CharacterPromptData { + pub token: CharacterToken, + pub is_active: bool, + pub character_name: Option, + pub kinovi_character_id: Option, + pub kinovi_character_name: Option, +} + +/// Look up multiple characters by their tokens, returning the data needed for prompting. +/// +/// Characters that are not found (or soft-deleted) are silently omitted from the results. +pub async fn batch_lookup_characters_by_token_for_prompting( + tokens: &[CharacterToken], + connection: &mut PoolConnection, +) -> Result, sqlx::Error> { + if tokens.is_empty() { + return Ok(Vec::new()); + } + + let mut query_builder: QueryBuilder = QueryBuilder::new( + r#" +SELECT + token, + is_active, + character_name, + kinovi_character_id, + kinovi_character_name +FROM characters +WHERE deleted_at IS NULL + AND token IN ( + "#, + ); + + let mut separated = query_builder.separated(", "); + for token in tokens { + separated.push_bind(token.as_str().to_string()); + } + + query_builder.push(")"); + + let results: Vec = query_builder + .build_query_as::() + .fetch_all(&mut **connection) + .await?; + + Ok(results) +} diff --git a/crates/schema/database/mysql_queries/src/queries/characters/create_pending_character.rs b/crates/schema/database/mysql_queries/src/queries/characters/create_pending_character.rs new file mode 100644 index 0000000000..02b8caf70c --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/characters/create_pending_character.rs @@ -0,0 +1,94 @@ +use log::info; +use sqlx::{Executor, MySql}; +use std::marker::PhantomData; + +use enums::by_table::characters::character_type::CharacterType; +use tokens::tokens::characters::CharacterToken; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use tokens::tokens::users::UserToken; + +/// Arguments for inserting a new pending character record. +pub struct CreatePendingCharacterArgs<'e, 'c, E> + where E: 'e + Executor<'c, Database = MySql> +{ + /// The type of character (e.g. KinoviSeedance). + pub character_type: CharacterType, + + /// User's display name for the character (truncated to 255 chars by caller). + pub character_name: &'e str, + + /// Optional user description (truncated to 512 chars by caller). + pub maybe_description: Option<&'e str>, + + /// The media file token of the original uploaded image. + pub maybe_original_upload_media_token: Option<&'e MediaFileToken>, + + /// The user who created this character. + pub maybe_creator_user_token: Option<&'e UserToken>, + + /// IP address for abuse tracking. + pub creator_ip_address: &'e str, + + /// The Kinovi character ID (e.g. "char_1774752056469_2wlxoq"). + pub kinovi_character_id: &'e str, + + /// The name Kinovi assigned to the character. + pub kinovi_character_name: &'e str, + + /// Our inference job token for tracking the character creation. + pub maybe_generic_inference_job_token: Option<&'e InferenceJobToken>, + + pub mysql_executor: E, + pub phantom: PhantomData<&'c E>, +} + +/// Insert a new pending character record. Returns the generated CharacterToken. +/// +/// The character is created with `is_active = false`. It will be activated +/// once the async creation job completes. +pub async fn create_pending_character<'e, 'c: 'e, E>( + args: CreatePendingCharacterArgs<'e, 'c, E>, +) -> Result + where E: 'e + Executor<'c, Database = MySql> +{ + let token = CharacterToken::generate(); + + let query = sqlx::query!( + r#" +INSERT INTO characters +SET + token = ?, + character_type = ?, + is_active = false, + character_name = ?, + maybe_description = ?, + maybe_avatar_media_token = NULL, + maybe_full_image_media_token = NULL, + maybe_original_upload_media_token = ?, + maybe_creator_user_token = ?, + creator_ip_address = ?, + kinovi_character_id = ?, + kinovi_character_name = ?, + maybe_kinovi_asset_id = NULL, + maybe_generic_inference_job_token = ? + "#, + token.as_str(), + args.character_type.to_str(), + args.character_name, + args.maybe_description, + args.maybe_original_upload_media_token.map(|t| t.as_str()), + args.maybe_creator_user_token.map(|t| t.as_str()), + args.creator_ip_address, + args.kinovi_character_id, + args.kinovi_character_name, + args.maybe_generic_inference_job_token.map(|t| t.as_str()), + ); + + let result = query.execute(args.mysql_executor) + .await?; + + info!("Created pending character {} (record ID {})", token, result.last_insert_id()); + + Ok(token) +} diff --git a/crates/schema/database/mysql_queries/src/queries/characters/delete_character.rs b/crates/schema/database/mysql_queries/src/queries/characters/delete_character.rs new file mode 100644 index 0000000000..2e2a2735dc --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/characters/delete_character.rs @@ -0,0 +1,26 @@ +use sqlx::pool::PoolConnection; +use sqlx::MySql; + +use tokens::tokens::characters::CharacterToken; + +/// Soft-delete a character by setting deleted_at to NOW(). +/// +/// This is idempotent — calling it on an already-deleted character is a no-op. +pub async fn delete_character( + character_token: &CharacterToken, + connection: &mut PoolConnection, +) -> Result<(), sqlx::Error> { + sqlx::query( + r#" +UPDATE characters +SET deleted_at = NOW() +WHERE token = ? + AND deleted_at IS NULL + "#, + ) + .bind(character_token.as_str()) + .execute(&mut **connection) + .await?; + + Ok(()) +} diff --git a/crates/schema/database/mysql_queries/src/queries/characters/get_character_by_token.rs b/crates/schema/database/mysql_queries/src/queries/characters/get_character_by_token.rs new file mode 100644 index 0000000000..9a744d77c1 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/characters/get_character_by_token.rs @@ -0,0 +1,54 @@ +use sqlx::pool::PoolConnection; +use sqlx::MySql; + +use enums::by_table::characters::character_type::CharacterType; +use tokens::tokens::characters::CharacterToken; +use tokens::tokens::media_files::MediaFileToken; +use tokens::tokens::users::UserToken; + +/// Full character record for the GET endpoint. +pub struct CharacterRecord { + pub token: CharacterToken, + pub character_type: CharacterType, + pub is_active: bool, + pub character_name: Option, + pub maybe_description: Option, + pub maybe_avatar_media_token: Option, + pub maybe_full_image_media_token: Option, + pub maybe_original_upload_media_token: Option, + pub maybe_creator_user_token: Option, + pub kinovi_character_id: Option, + pub kinovi_character_name: Option, + pub maybe_kinovi_asset_id: Option, +} + +/// Look up a character by token (excluding soft-deleted). +pub async fn get_character_by_token( + character_token: &CharacterToken, + connection: &mut PoolConnection, +) -> Result, sqlx::Error> { + sqlx::query_as!( + CharacterRecord, + r#" +SELECT + token as `token: tokens::tokens::characters::CharacterToken`, + character_type as `character_type: enums::by_table::characters::character_type::CharacterType`, + is_active as `is_active: bool`, + character_name, + maybe_description, + maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`, + maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`, + maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`, + maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`, + kinovi_character_id, + kinovi_character_name, + maybe_kinovi_asset_id +FROM characters +WHERE token = ? + AND deleted_at IS NULL + "#, + character_token.as_str(), + ) + .fetch_optional(&mut **connection) + .await +} diff --git a/crates/schema/database/mysql_queries/src/queries/characters/get_character_by_token_including_deleted.rs b/crates/schema/database/mysql_queries/src/queries/characters/get_character_by_token_including_deleted.rs new file mode 100644 index 0000000000..4a43c35e50 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/characters/get_character_by_token_including_deleted.rs @@ -0,0 +1,39 @@ +use sqlx::pool::PoolConnection; +use sqlx::{FromRow, MySql}; + +use tokens::tokens::characters::CharacterToken; +use tokens::tokens::users::UserToken; + +/// Minimal character record for ownership and deletion checks. +#[derive(Debug, FromRow)] +pub struct CharacterOwnershipRecord { + pub token: CharacterToken, + pub maybe_creator_user_token: Option, + pub is_active: bool, + pub is_deleted: bool, +} + +/// Look up a character by token, including soft-deleted records. +/// +/// Returns `None` if the character does not exist at all. +pub async fn get_character_by_token_including_deleted( + character_token: &CharacterToken, + connection: &mut PoolConnection, +) -> Result, sqlx::Error> { + let result = sqlx::query_as::<_, CharacterOwnershipRecord>( + r#" +SELECT + token, + maybe_creator_user_token, + is_active, + (deleted_at IS NOT NULL) as is_deleted +FROM characters +WHERE token = ? + "#, + ) + .bind(character_token.as_str()) + .fetch_optional(&mut **connection) + .await?; + + Ok(result) +} diff --git a/crates/schema/database/mysql_queries/src/queries/characters/list_active_characters_for_user.rs b/crates/schema/database/mysql_queries/src/queries/characters/list_active_characters_for_user.rs new file mode 100644 index 0000000000..75d1d76292 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/characters/list_active_characters_for_user.rs @@ -0,0 +1,86 @@ +use sqlx::pool::PoolConnection; +use sqlx::MySql; + +use enums::by_table::characters::character_type::CharacterType; +use tokens::tokens::characters::CharacterToken; +use tokens::tokens::media_files::MediaFileToken; +use tokens::tokens::users::UserToken; + +const DEFAULT_PAGE_SIZE: u32 = 50; + +/// A character row for the list endpoint. +pub struct CharacterListRow { + pub id: u64, + pub token: CharacterToken, + pub character_type: CharacterType, + pub is_active: bool, + pub character_name: Option, + pub maybe_description: Option, + pub maybe_avatar_media_token: Option, + pub maybe_full_image_media_token: Option, + pub maybe_original_upload_media_token: Option, + pub maybe_creator_user_token: Option, + pub kinovi_character_id: Option, + pub kinovi_character_name: Option, + pub maybe_kinovi_asset_id: Option, +} + +pub struct ListActiveCharactersResult { + pub characters: Vec, + pub next_cursor: Option, +} + +/// List active, non-deleted characters for a user, paginated by descending id. +pub async fn list_active_characters_for_user( + user_token: &UserToken, + maybe_cursor: Option, + connection: &mut PoolConnection, +) -> Result { + let fetch_limit = (DEFAULT_PAGE_SIZE + 1) as i64; + let id_cursor = maybe_cursor.map(|c| c as i64).unwrap_or(i64::MAX); + + let rows = sqlx::query_as!( + CharacterListRow, + r#" +SELECT + id as `id: u64`, + token as `token: tokens::tokens::characters::CharacterToken`, + character_type as `character_type: enums::by_table::characters::character_type::CharacterType`, + is_active as `is_active: bool`, + character_name, + maybe_description, + maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`, + maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`, + maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`, + maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`, + kinovi_character_id, + kinovi_character_name, + maybe_kinovi_asset_id +FROM characters +WHERE maybe_creator_user_token = ? + AND is_active = true + AND deleted_at IS NULL + AND id < ? +ORDER BY id DESC +LIMIT ? + "#, + user_token.as_str(), + id_cursor, + fetch_limit, + ) + .fetch_all(&mut **connection) + .await?; + + let has_next = rows.len() as u32 >= DEFAULT_PAGE_SIZE; + let characters: Vec = rows.into_iter().take(DEFAULT_PAGE_SIZE as usize).collect(); + let next_cursor = if has_next { + characters.last().map(|c| c.id) + } else { + None + }; + + Ok(ListActiveCharactersResult { + characters, + next_cursor, + }) +} diff --git a/crates/schema/database/mysql_queries/src/queries/characters/mod.rs b/crates/schema/database/mysql_queries/src/queries/characters/mod.rs new file mode 100644 index 0000000000..92dcb32bae --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/characters/mod.rs @@ -0,0 +1,7 @@ +pub mod batch_lookup_characters_by_token_for_prompting; +pub mod create_pending_character; +pub mod delete_character; +pub mod get_character_by_token; +pub mod get_character_by_token_including_deleted; +pub mod list_active_characters_for_user; +pub mod update_character_name_and_description; diff --git a/crates/schema/database/mysql_queries/src/queries/characters/update_character_name_and_description.rs b/crates/schema/database/mysql_queries/src/queries/characters/update_character_name_and_description.rs new file mode 100644 index 0000000000..c0637a3bd5 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/characters/update_character_name_and_description.rs @@ -0,0 +1,36 @@ +use sqlx::pool::PoolConnection; +use sqlx::MySql; + +use tokens::tokens::characters::CharacterToken; + +/// Update a character's name, kinovi name, and/or description. +/// +/// All fields are always SET in the query — the caller should pass the +/// existing values for fields that should not change. +pub async fn update_character_name_and_description( + character_token: &CharacterToken, + character_name: &str, + kinovi_character_name: &str, + maybe_description: Option<&str>, + connection: &mut PoolConnection, +) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" +UPDATE characters +SET + character_name = ?, + kinovi_character_name = ?, + maybe_description = ? +WHERE token = ? + AND deleted_at IS NULL + "#, + character_name, + kinovi_character_name, + maybe_description, + character_token.as_str(), + ) + .execute(&mut **connection) + .await?; + + Ok(()) +} diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/seedance2pro/insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/seedance2pro/insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token.rs new file mode 100644 index 0000000000..f1f070578c --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/seedance2pro/insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token.rs @@ -0,0 +1,125 @@ +use anyhow::anyhow; +use log::info; +use sqlx::{Executor, MySql}; +use std::marker::PhantomData; + +use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory; +use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty; +use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory; +use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType; +use enums::common::job_status_plus::JobStatusPlus; +use enums::common::visibility::Visibility; +use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::users::UserToken; + +use crate::errors::database_query_error::DatabaseQueryError; + +pub struct InsertGenericInferenceForSeedance2ProCharacterWithAprioriJobTokenArgs<'e, 'c, E> + where E: 'e + Executor<'c, Database = MySql> +{ + pub uuid_idempotency_token: &'e str, + + /// Pre-generated job token. + pub apriori_job_token: &'e InferenceJobToken, + + /// The Kinovi character_id returned by generate_character(). + pub kinovi_character_id: &'e str, + + pub maybe_creator_user_token: Option<&'e UserToken>, + pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>, + pub creator_ip_address: &'e str, + pub creator_set_visibility: Visibility, + + pub mysql_executor: E, + pub phantom: PhantomData<&'c E>, +} + +pub async fn insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token<'e, 'c : 'e, E>( + args: InsertGenericInferenceForSeedance2ProCharacterWithAprioriJobTokenArgs<'e, 'c, E> +) -> Result + where E: 'e + Executor<'c, Database = MySql> +{ + const JOB_TYPE: InferenceJobType = InferenceJobType::Seedance2ProCharacter; + const INFERENCE_CATEGORY: InferenceCategory = InferenceCategory::CharacterGeneration; + const PRODUCT_CATEGORY: InferenceJobProductCategory = InferenceJobProductCategory::Seedance2ProCharacter; + const EXTERNAL_THIRD_PARTY: InferenceJobExternalThirdParty = InferenceJobExternalThirdParty::Seedance2Pro; + const STATUS: JobStatusPlus = JobStatusPlus::Pending; + + let query = sqlx::query!( + r#" +INSERT INTO generic_inference_jobs +SET + token = ?, + uuid_idempotency_token = ?, + + job_type = ?, + + maybe_external_third_party = ?, + maybe_external_third_party_id = ?, + + product_category = ?, + inference_category = ?, + + maybe_model_type = NULL, + maybe_model_token = NULL, + + maybe_input_source_token = NULL, + maybe_input_source_token_type = NULL, + + maybe_download_url = NULL, + maybe_cover_image_media_file_token = NULL, + + maybe_prompt_token = NULL, + + maybe_wallet_ledger_entry_token = NULL, + + maybe_raw_inference_text = NULL, + + maybe_inference_args = NULL, + + maybe_creator_user_token = ?, + maybe_creator_anonymous_visitor_token = ?, + creator_ip_address = ?, + creator_set_visibility = ?, + + priority_level = 0, + is_keepalive_required = FALSE, + max_duration_seconds = 0, + + is_debug_request = FALSE, + maybe_routing_tag = NULL, + + status = ? + "#, + args.apriori_job_token.as_str(), + args.uuid_idempotency_token, + + JOB_TYPE.to_str(), + + EXTERNAL_THIRD_PARTY.to_str(), + args.kinovi_character_id, + + PRODUCT_CATEGORY.to_str(), + INFERENCE_CATEGORY.to_str(), + + args.maybe_creator_user_token.map(|t| t.to_string()), + args.maybe_avt_token.map(|t| t.to_string()), + args.creator_ip_address, + args.creator_set_visibility.to_str(), + + STATUS.to_str(), + ); + + let query_result = query.execute(args.mysql_executor) + .await; + + let record_id = match query_result { + Err(err) => return Err(DatabaseQueryError::from(err)), + Ok(res) => res.last_insert_id(), + }; + + info!("Insert generic inference job for Seedance2Pro character: {} with record ID {}", args.apriori_job_token, record_id); + + Ok(args.apriori_job_token.clone()) +} diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/seedance2pro/mod.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/seedance2pro/mod.rs index 24c6ee0ef4..55a8b34b9c 100644 --- a/crates/schema/database/mysql_queries/src/queries/generic_inference/seedance2pro/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/seedance2pro/mod.rs @@ -1,2 +1,3 @@ +pub mod insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token; pub mod insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token; pub mod list_pending_seedance2pro_jobs; diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/web/batch_get_inference_job_status.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/web/batch_get_inference_job_status.rs index 848ab3936f..bb83d62979 100644 --- a/crates/schema/database/mysql_queries/src/queries/generic_inference/web/batch_get_inference_job_status.rs +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/web/batch_get_inference_job_status.rs @@ -187,6 +187,7 @@ fn raw_records_to_public_result(records: Vec) -> V InferenceCategory::SeedVc => Some("Seed VC"), InferenceCategory::ConvertBvhToWorkflow => Some("BVH to Workflow"), InferenceCategory::SplatGeneration => Some("Splat Generation"), + InferenceCategory::CharacterGeneration => Some("Character Generation"), InferenceCategory::DeprecatedField => Some("Job"), // TODO(bt,2024-07-16): Fix }; } @@ -210,6 +211,7 @@ fn raw_records_to_public_result(records: Vec) -> V InferenceCategory::LivePortrait => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), InferenceCategory::ConvertBvhToWorkflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), InferenceCategory::SplatGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), + InferenceCategory::CharacterGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), InferenceCategory::DeprecatedField => (true, None), // TODO(bt,2024-07-16): We'll need to read another field! }; diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/web/get_inference_job_status.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/web/get_inference_job_status.rs index 37a3742e83..926ccf7e56 100644 --- a/crates/schema/database/mysql_queries/src/queries/generic_inference/web/get_inference_job_status.rs +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/web/get_inference_job_status.rs @@ -162,6 +162,7 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI InferenceCategory::SeedVc => Some("Seed VC"), InferenceCategory::ConvertBvhToWorkflow => Some("BVH to Workflow"), InferenceCategory::SplatGeneration => Some("Splat Generation"), + InferenceCategory::CharacterGeneration => Some("Character Generation"), InferenceCategory::DeprecatedField => Some("Job"), // TODO(bt,2024-07-16): Fix }; } @@ -185,6 +186,7 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI InferenceCategory::SeedVc => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), InferenceCategory::ConvertBvhToWorkflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), InferenceCategory::SplatGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), + InferenceCategory::CharacterGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), InferenceCategory::DeprecatedField => (true, None), // TODO(bt,2024-07-16): We'll need to read another field! }; diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/web/list_session_jobs.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/web/list_session_jobs.rs index 3030a8294c..ed4ee37d3d 100644 --- a/crates/schema/database/mysql_queries/src/queries/generic_inference/web/list_session_jobs.rs +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/web/list_session_jobs.rs @@ -253,6 +253,7 @@ fn raw_records_to_public_result(records: Vec) -> V InferenceCategory::LivePortrait => Some("Live Portrait"), InferenceCategory::SeedVc => Some("Seed VC"), InferenceCategory::SplatGeneration => Some("Splat Generation"), + InferenceCategory::CharacterGeneration => Some("Character Generation"), InferenceCategory::DeprecatedField => Some("Job"), // TODO(bt,2024-07-16): Fix }; } @@ -276,6 +277,7 @@ fn raw_records_to_public_result(records: Vec) -> V InferenceCategory::SeedVc => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), InferenceCategory::ConvertBvhToWorkflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), InferenceCategory::SplatGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), + InferenceCategory::CharacterGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()), InferenceCategory::DeprecatedField => (true, None), // TODO(bt,2024-07-16): We'll need to read another field! }; diff --git a/crates/schema/database/mysql_queries/src/queries/mod.rs b/crates/schema/database/mysql_queries/src/queries/mod.rs index a51375e819..1d30e9f6c6 100644 --- a/crates/schema/database/mysql_queries/src/queries/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/mod.rs @@ -6,6 +6,7 @@ pub mod batch_generations; pub mod beta_keys; pub mod billing; pub mod browser_session_logs; +pub mod characters; pub mod comments; pub mod database_time; pub mod email_sender_jobs; diff --git a/crates/schema/public/enums/src/by_table/characters/character_type.rs b/crates/schema/public/enums/src/by_table/characters/character_type.rs new file mode 100644 index 0000000000..294389014e --- /dev/null +++ b/crates/schema/public/enums/src/by_table/characters/character_type.rs @@ -0,0 +1,108 @@ +use std::collections::BTreeSet; + +#[cfg(test)] +use strum::EnumCount; +#[cfg(test)] +use strum::EnumIter; + +/// Used in the `characters` table in `VARCHAR(24)` field `character_type`. +/// +/// Identifies the type/backend used to create the character. +/// +/// YOU CAN ADD NEW VALUES, BUT DO NOT CHANGE EXISTING VALUES WITHOUT A MIGRATION STRATEGY. +#[cfg_attr(test, derive(EnumIter, EnumCount))] +#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)] +pub enum CharacterType { + /// Character created via Kinovi / Seedance2Pro. + #[serde(rename = "kinovi_seedance")] + KinoviSeedance, +} + +impl_enum_display_and_debug_using_to_str!(CharacterType); +impl_mysql_enum_coders!(CharacterType); + +impl CharacterType { + pub fn to_str(&self) -> &'static str { + match self { + Self::KinoviSeedance => "kinovi_seedance", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "kinovi_seedance" => Ok(Self::KinoviSeedance), + _ => Err(format!("invalid value: {:?}", value)), + } + } + + pub fn all_variants() -> BTreeSet { + BTreeSet::from([ + Self::KinoviSeedance, + ]) + } +} + +#[cfg(test)] +mod tests { + use crate::by_table::characters::character_type::CharacterType; + use crate::test_helpers::assert_serialization; + + mod explicit_checks { + use super::*; + + #[test] + fn test_serialization() { + assert_serialization(CharacterType::KinoviSeedance, "kinovi_seedance"); + } + + #[test] + fn to_str() { + assert_eq!(CharacterType::KinoviSeedance.to_str(), "kinovi_seedance"); + } + + #[test] + fn from_str() { + assert_eq!(CharacterType::from_str("kinovi_seedance").unwrap(), CharacterType::KinoviSeedance); + assert!(CharacterType::from_str("invalid").is_err()); + } + + #[test] + fn all_variants() { + const EXPECTED_COUNT: usize = 1; + + assert_eq!(CharacterType::all_variants().len(), EXPECTED_COUNT); + + use strum::IntoEnumIterator; + assert_eq!(CharacterType::all_variants().len(), CharacterType::iter().len()); + } + } + + mod mechanical_checks { + use super::*; + + #[test] + fn variant_length() { + use strum::IntoEnumIterator; + assert_eq!(CharacterType::all_variants().len(), CharacterType::iter().len()); + } + + #[test] + fn round_trip() { + for variant in CharacterType::all_variants() { + assert_eq!(variant, CharacterType::from_str(variant.to_str()).unwrap()); + assert_eq!(variant, CharacterType::from_str(&format!("{}", variant)).unwrap()); + assert_eq!(variant, CharacterType::from_str(&format!("{:?}", variant)).unwrap()); + } + } + + #[test] + fn serialized_length_ok_for_database() { + const MAX_LENGTH: usize = 24; + for variant in CharacterType::all_variants() { + let serialized = variant.to_str(); + assert!(!serialized.is_empty(), "variant {:?} is too short", variant); + assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long for VARCHAR({})", variant, MAX_LENGTH); + } + } + } +} diff --git a/crates/schema/public/enums/src/by_table/characters/mod.rs b/crates/schema/public/enums/src/by_table/characters/mod.rs new file mode 100644 index 0000000000..d7b62c5e2b --- /dev/null +++ b/crates/schema/public/enums/src/by_table/characters/mod.rs @@ -0,0 +1 @@ +pub mod character_type; diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_category.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_category.rs index 226a6af7db..64a620025b 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_category.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_category.rs @@ -52,6 +52,10 @@ pub enum InferenceCategory { #[serde(rename = "splat_generation")] SplatGeneration, + /// Character generation (eg. Kinovi/Seedance2Pro characters) + #[serde(rename = "character_generation")] + CharacterGeneration, + /// FAL-powered image background removal #[serde(rename = "background_removal")] BackgroundRemoval, @@ -109,6 +113,7 @@ impl InferenceCategory { Self::VideoGeneration => "video_generation", Self::ObjectGeneration => "object_generation", Self::SplatGeneration => "splat_generation", + Self::CharacterGeneration => "character_generation", Self::BackgroundRemoval => "background_removal", Self::Mocap => "mocap", Self::Workflow => "workflow", @@ -131,6 +136,7 @@ impl InferenceCategory { "video_generation" => Ok(Self::VideoGeneration), "object_generation" => Ok(Self::ObjectGeneration), "splat_generation" => Ok(Self::SplatGeneration), + "character_generation" => Ok(Self::CharacterGeneration), "background_removal" => Ok(Self::BackgroundRemoval), "f5_tts" => Ok(Self::F5TTS), "mocap" => Ok(Self::Mocap), @@ -155,6 +161,7 @@ impl InferenceCategory { Self::ImageGeneration, Self::ObjectGeneration, Self::SplatGeneration, + Self::CharacterGeneration, Self::VideoGeneration, Self::BackgroundRemoval, Self::Mocap, @@ -187,6 +194,7 @@ mod tests { assert_serialization(InferenceCategory::VideoGeneration, "video_generation"); assert_serialization(InferenceCategory::ObjectGeneration, "object_generation"); assert_serialization(InferenceCategory::SplatGeneration, "splat_generation"); + assert_serialization(InferenceCategory::CharacterGeneration, "character_generation"); assert_serialization(InferenceCategory::BackgroundRemoval, "background_removal"); assert_serialization(InferenceCategory::Mocap, "mocap"); assert_serialization(InferenceCategory::F5TTS, "f5_tts"); @@ -208,6 +216,7 @@ mod tests { assert_eq!(InferenceCategory::VideoGeneration.to_str(), "video_generation"); assert_eq!(InferenceCategory::ObjectGeneration.to_str(), "object_generation"); assert_eq!(InferenceCategory::SplatGeneration.to_str(), "splat_generation"); + assert_eq!(InferenceCategory::CharacterGeneration.to_str(), "character_generation"); assert_eq!(InferenceCategory::BackgroundRemoval.to_str(), "background_removal"); assert_eq!(InferenceCategory::F5TTS.to_str(), "f5_tts"); assert_eq!(InferenceCategory::SeedVc.to_str(), "seed_vc"); @@ -229,6 +238,7 @@ mod tests { assert_eq!(InferenceCategory::from_str("video_generation").unwrap(), InferenceCategory::VideoGeneration); assert_eq!(InferenceCategory::from_str("object_generation").unwrap(), InferenceCategory::ObjectGeneration); assert_eq!(InferenceCategory::from_str("splat_generation").unwrap(), InferenceCategory::SplatGeneration); + assert_eq!(InferenceCategory::from_str("character_generation").unwrap(), InferenceCategory::CharacterGeneration); assert_eq!(InferenceCategory::from_str("background_removal").unwrap(), InferenceCategory::BackgroundRemoval); assert_eq!(InferenceCategory::from_str("f5_tts").unwrap(), InferenceCategory::F5TTS); assert_eq!(InferenceCategory::from_str("seed_vc").unwrap(), InferenceCategory::SeedVc); @@ -243,7 +253,7 @@ mod tests { #[test] fn all_variants() { // Static check - const EXPECTED_COUNT : usize = 17; + const EXPECTED_COUNT : usize = 18; assert_eq!(InferenceCategory::all_variants().len(), EXPECTED_COUNT); assert_eq!(InferenceCategory::iter().len(), EXPECTED_COUNT); diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs index 42fca57b78..b2a6182d4b 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs @@ -35,6 +35,9 @@ pub enum InferenceJobProductCategory { #[serde(rename = "seedance2pro_video")] Seedance2ProVideo, + #[serde(rename = "seedance2pro_character")] + Seedance2ProCharacter, + // =============== WORLD LABS =============== /// World Labs: Gaussian Splat Generation @@ -137,6 +140,7 @@ impl InferenceJobProductCategory { Self::FalObject => "fal_object", Self::FalBgRemoval => "fal_bg_removal", Self::Seedance2ProVideo => "seedance2pro_video", + Self::Seedance2ProCharacter => "seedance2pro_character", Self::WorldlabsSplat => "worldlabs_splat", Self::TtsGptSoVits => "tts_gpt_so_vits", Self::TtsStyleTts2 => "tts_style_tts2", @@ -171,6 +175,7 @@ impl InferenceJobProductCategory { "fal_object" => Ok(Self::FalObject), "fal_bg_removal" => Ok(Self::FalBgRemoval), "seedance2pro_video" => Ok(Self::Seedance2ProVideo), + "seedance2pro_character" => Ok(Self::Seedance2ProCharacter), "worldlabs_splat" => Ok(Self::WorldlabsSplat), "tts_gpt_so_vits" => Ok(Self::TtsGptSoVits), "tts_style_tts2" => Ok(Self::TtsStyleTts2), @@ -208,6 +213,7 @@ impl InferenceJobProductCategory { Self::FalObject, Self::FalBgRemoval, Self::Seedance2ProVideo, + Self::Seedance2ProCharacter, Self::WorldlabsSplat, Self::TtsGptSoVits, Self::TtsStyleTts2, @@ -251,6 +257,7 @@ mod tests { assert_serialization(InferenceJobProductCategory::FalObject, "fal_object"); assert_serialization(InferenceJobProductCategory::FalBgRemoval, "fal_bg_removal"); assert_serialization(InferenceJobProductCategory::Seedance2ProVideo, "seedance2pro_video"); + assert_serialization(InferenceJobProductCategory::Seedance2ProCharacter, "seedance2pro_character"); assert_serialization(InferenceJobProductCategory::WorldlabsSplat, "worldlabs_splat"); assert_serialization(InferenceJobProductCategory::TtsGptSoVits, "tts_gpt_so_vits"); assert_serialization(InferenceJobProductCategory::TtsStyleTts2, "tts_style_tts2"); @@ -284,6 +291,7 @@ mod tests { assert_eq!(InferenceJobProductCategory::FalObject.to_str(), "fal_object"); assert_eq!(InferenceJobProductCategory::FalBgRemoval.to_str(), "fal_bg_removal"); assert_eq!(InferenceJobProductCategory::Seedance2ProVideo.to_str(), "seedance2pro_video"); + assert_eq!(InferenceJobProductCategory::Seedance2ProCharacter.to_str(), "seedance2pro_character"); assert_eq!(InferenceJobProductCategory::WorldlabsSplat.to_str(), "worldlabs_splat"); assert_eq!(InferenceJobProductCategory::TtsGptSoVits.to_str(), "tts_gpt_so_vits"); assert_eq!(InferenceJobProductCategory::TtsStyleTts2.to_str(), "tts_style_tts2"); @@ -316,6 +324,7 @@ mod tests { assert_eq!(InferenceJobProductCategory::from_str("fal_object").unwrap(), InferenceJobProductCategory::FalObject); assert_eq!(InferenceJobProductCategory::from_str("fal_bg_removal").unwrap(), InferenceJobProductCategory::FalBgRemoval); assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video").unwrap(), InferenceJobProductCategory::Seedance2ProVideo); + assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_character").unwrap(), InferenceJobProductCategory::Seedance2ProCharacter); assert_eq!(InferenceJobProductCategory::from_str("worldlabs_splat").unwrap(), InferenceJobProductCategory::WorldlabsSplat); assert_eq!(InferenceJobProductCategory::from_str("tts_gpt_so_vits").unwrap(), InferenceJobProductCategory::TtsGptSoVits); assert_eq!(InferenceJobProductCategory::from_str("tts_style_tts2").unwrap(), InferenceJobProductCategory::TtsStyleTts2); @@ -343,7 +352,7 @@ mod tests { #[test] fn all_variants() { // Static check - const EXPECTED_COUNT : usize = 29; + const EXPECTED_COUNT : usize = 30; assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT); assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT); diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs index 888f8a4889..9990be8360 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs @@ -23,6 +23,10 @@ pub enum InferenceJobType { #[serde(rename = "seedance2pro_queue")] Seedance2ProQueue, + /// Seedance 2 Pro character creation. We poll for results. + #[serde(rename = "seedance2pro_character")] + Seedance2ProCharacter, + /// World Labs jobs. We poll for results. #[serde(rename = "worldlabs_queue")] WorldlabsQueue, @@ -119,6 +123,7 @@ impl InferenceJobType { match self { Self::FalQueue => "fal_queue", Self::Seedance2ProQueue => "seedance2pro_queue", + Self::Seedance2ProCharacter => "seedance2pro_character", Self::WorldlabsQueue => "worldlabs_queue", Self::VideoRender => "video_render", Self::LivePortrait => "live_portrait", @@ -147,6 +152,7 @@ impl InferenceJobType { match value { "fal_queue" => Ok(Self::FalQueue), "seedance2pro_queue" => Ok(Self::Seedance2ProQueue), + "seedance2pro_character" => Ok(Self::Seedance2ProCharacter), "worldlabs_queue" => Ok(Self::WorldlabsQueue), "video_render" => Ok(Self::VideoRender), "live_portrait" => Ok(Self::LivePortrait), @@ -178,6 +184,7 @@ impl InferenceJobType { BTreeSet::from([ Self::FalQueue, Self::Seedance2ProQueue, + Self::Seedance2ProCharacter, Self::WorldlabsQueue, Self::VideoRender, Self::LivePortrait, @@ -220,6 +227,7 @@ mod tests { fn test_serialization() { assert_serialization(InferenceJobType::FalQueue, "fal_queue"); assert_serialization(InferenceJobType::Seedance2ProQueue, "seedance2pro_queue"); + assert_serialization(InferenceJobType::Seedance2ProCharacter, "seedance2pro_character"); assert_serialization(InferenceJobType::WorldlabsQueue, "worldlabs_queue"); assert_serialization(InferenceJobType::VideoRender, "video_render"); assert_serialization(InferenceJobType::LivePortrait, "live_portrait"); @@ -247,6 +255,7 @@ mod tests { fn to_str() { assert_eq!(InferenceJobType::FalQueue.to_str(), "fal_queue"); assert_eq!(InferenceJobType::Seedance2ProQueue.to_str(), "seedance2pro_queue"); + assert_eq!(InferenceJobType::Seedance2ProCharacter.to_str(), "seedance2pro_character"); assert_eq!(InferenceJobType::WorldlabsQueue.to_str(), "worldlabs_queue"); assert_eq!(InferenceJobType::VideoRender.to_str(), "video_render"); assert_eq!(InferenceJobType::LivePortrait.to_str(), "live_portrait"); @@ -274,6 +283,7 @@ mod tests { fn from_str() { assert_eq!(InferenceJobType::from_str("fal_queue").unwrap(), InferenceJobType::FalQueue); assert_eq!(InferenceJobType::from_str("seedance2pro_queue").unwrap(), InferenceJobType::Seedance2ProQueue); + assert_eq!(InferenceJobType::from_str("seedance2pro_character").unwrap(), InferenceJobType::Seedance2ProCharacter); assert_eq!(InferenceJobType::from_str("worldlabs_queue").unwrap(), InferenceJobType::WorldlabsQueue); assert_eq!(InferenceJobType::from_str("video_render").unwrap(), InferenceJobType::VideoRender); assert_eq!(InferenceJobType::from_str("live_portrait").unwrap(), InferenceJobType::LivePortrait); @@ -300,7 +310,7 @@ mod tests { #[test] fn all_variants() { // Static check - const EXPECTED_COUNT : usize = 23; + const EXPECTED_COUNT : usize = 24; assert_eq!(InferenceJobType::all_variants().len(), EXPECTED_COUNT); assert_eq!(InferenceJobType::iter().len(), EXPECTED_COUNT); diff --git a/crates/schema/public/enums/src/by_table/mod.rs b/crates/schema/public/enums/src/by_table/mod.rs index bd524375c4..6e48fec55b 100644 --- a/crates/schema/public/enums/src/by_table/mod.rs +++ b/crates/schema/public/enums/src/by_table/mod.rs @@ -1,6 +1,7 @@ // ===== MySql ===== pub mod audit_logs; pub mod batch_generations; +pub mod characters; pub mod beta_keys; pub mod comments; pub mod email_sender_jobs; diff --git a/crates/schema/public/enums_public/src/by_table/generic_inference_jobs/public_inference_job_type.rs b/crates/schema/public/enums_public/src/by_table/generic_inference_jobs/public_inference_job_type.rs index c480b5ae79..9a3ac1373b 100644 --- a/crates/schema/public/enums_public/src/by_table/generic_inference_jobs/public_inference_job_type.rs +++ b/crates/schema/public/enums_public/src/by_table/generic_inference_jobs/public_inference_job_type.rs @@ -105,6 +105,10 @@ pub enum PublicInferenceJobType { #[serde(rename = "seedance2pro_queue")] Seedance2ProQueue, + /// Seedance 2 Pro character generation jobs. We poll for results. + #[serde(rename = "seedance2pro_character")] + Seedance2ProCharacter, + /// World Labs splat generation jobs. We poll for results. #[serde(rename = "worldlabs_queue")] WorldlabsQueue, @@ -139,6 +143,7 @@ impl PublicInferenceJobType { InferenceJobType::RerenderAVideo => Self::RerenderAVideo, InferenceJobType::ImageGenApi => Self::ImageGenApi, InferenceJobType::Seedance2ProQueue => Self::Seedance2ProQueue, + InferenceJobType::Seedance2ProCharacter => Self::Seedance2ProCharacter, InferenceJobType::WorldlabsQueue => Self::WorldlabsQueue, } } @@ -164,6 +169,7 @@ impl PublicInferenceJobType { Self::StableDiffusion => InferenceJobType::StableDiffusion, Self::ImageGenApi => InferenceJobType::ImageGenApi, Self::Seedance2ProQueue => InferenceJobType::Seedance2ProQueue, + Self::Seedance2ProCharacter => InferenceJobType::Seedance2ProCharacter, Self::WorldlabsQueue => InferenceJobType::WorldlabsQueue, Self::StyleTTS2 => InferenceJobType::StyleTTS2, Self::Tacotron2 => InferenceJobType::Tacotron2, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/create_character_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/create_character_handler.rs index 1f0615fdd1..fa8fd2d40e 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/create_character_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/create_character_handler.rs @@ -1,12 +1,36 @@ +use std::marker::PhantomData; use std::sync::Arc; use actix_web::web::Json; use actix_web::{web, HttpRequest}; +use log::{error, info, warn}; +use sqlx::Acquire; use artcraft_api_defs::characters::create_character::{CreateCharacterRequest, CreateCharacterResponse}; +use enums::by_table::characters::character_type::CharacterType; +use enums::common::visibility::Visibility; +use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::queries::characters::create_pending_character::{create_pending_character, CreatePendingCharacterArgs}; +use mysql_queries::queries::generic_inference::seedance2pro::insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token::{ + insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token, + InsertGenericInferenceForSeedance2ProCharacterWithAprioriJobTokenArgs, +}; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession; +use seedance2pro_client::requests::generate_character::generate_character::{generate_character, GenerateCharacterArgs}; +use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs}; +use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use url_utils::extension::extract_extension_from_url::{extract_extension_from_url, ExtractExtensions}; -use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; use crate::state::server_state::ServerState; +use crate::util::http_download_url_to_bytes::http_download_url_to_bytes; +use crate::util::lookup::lookup_media_file_urls_as_map::lookup_media_file_urls_as_map; + +const MAX_NAME_LENGTH: usize = 255; +const MAX_DESCRIPTION_LENGTH: usize = 512; /// Create a new character. #[utoipa::path( @@ -18,14 +42,227 @@ use crate::state::server_state::ServerState; (status = 200, description = "Success", body = CreateCharacterResponse), (status = 400, description = "Bad input"), (status = 401, description = "Unauthorized"), - (status = 402, description = "Payment required"), (status = 500, description = "Server error"), ), )] pub async fn create_character_handler( - _http_request: HttpRequest, - _request: Json, - _server_state: web::Data>, -) -> Result, CommonWebError> { - todo!() + http_request: HttpRequest, + request: Json, + server_state: web::Data>, +) -> Result, AdvancedCommonWebError> { + + // --- Auth --- + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + AdvancedCommonWebError::from_anyhow_error(e) + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => return Err(AdvancedCommonWebError::NotAuthorized), + }; + + let user_token = &user_session.user_token; + let ip_address = get_request_ip(&http_request); + + // --- Validate input --- + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason)); + } + + // For now we use the model name as the character name and description. + let character_name = request.character_name.clone(); + let character_name = truncate_string(&character_name, MAX_NAME_LENGTH); + let character_description = request.character_description.as_deref() + .map(|desc| desc.trim()); + + // --- Look up the image media file --- + + let file_urls_by_token = lookup_media_file_urls_as_map( + &http_request, + &mut mysql_connection, + server_state.server_environment, + &[request.image_media_token.clone()], + ).await?; + + let image_cdn_url = file_urls_by_token + .get(&request.image_media_token) + .ok_or_else(|| { + warn!("Image media token not found: {:?}", request.image_media_token); + AdvancedCommonWebError::BadInputWithSimpleMessage("Image media file not found.".to_string()) + })?; + + // --- Insert idempotency token --- + + insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + // --- Upload image to Kinovi CDN --- + + let seedance_image_url = upload_to_kinovi( + &server_state.seedance2pro.cookies, + image_cdn_url, + ).await?; + + info!("Uploaded image to Kinovi: {}", seedance_image_url); + + // --- Call Kinovi generate_character --- + + let session = Seedance2ProSession::from_cookies_string( + server_state.seedance2pro.cookies.clone() + ); + + let gen_result = generate_character(GenerateCharacterArgs { + session: &session, + name: character_name.clone(), + description: character_description + .map(|d| d.to_string()) + .unwrap_or_else(|| "".to_string()), + reference_image_url: seedance_image_url, + is_public: false, + host_override: None, + }) + .await + .map_err(|err| { + error!("Error calling Kinovi generate_character: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + info!( + "Kinovi character created: id={}, character_id={}, name={}", + gen_result.id, gen_result.character_id, gen_result.name, + ); + + // --- Generate apriori job token --- + + let apriori_job_token = InferenceJobToken::generate(); + + // --- Insert records in a transaction --- + + let mut transaction = mysql_connection + .begin() + .await + .map_err(|err| { + error!("Error starting MySQL transaction: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + let character_token = create_pending_character(CreatePendingCharacterArgs { + character_type: CharacterType::KinoviSeedance, + character_name: &character_name, + maybe_description: character_description, + maybe_original_upload_media_token: Some(&request.image_media_token), + maybe_creator_user_token: Some(user_token), + creator_ip_address: &ip_address, + kinovi_character_id: &gen_result.character_id, + kinovi_character_name: &gen_result.name, + maybe_generic_inference_job_token: Some(&apriori_job_token), + mysql_executor: &mut *transaction, + phantom: PhantomData, + }) + .await?; + + info!("Created pending character: {}", character_token); + + insert_generic_inference_job_for_seedance2pro_character_with_apriori_job_token( + InsertGenericInferenceForSeedance2ProCharacterWithAprioriJobTokenArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + apriori_job_token: &apriori_job_token, + kinovi_character_id: &gen_result.character_id, + maybe_creator_user_token: Some(user_token), + maybe_avt_token: None, + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Hidden, + mysql_executor: &mut *transaction, + phantom: PhantomData, + } + ) + .await + .map_err(|err| { + error!("Error inserting inference job: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + info!("Created inference job: {}", apriori_job_token); + + transaction + .commit() + .await + .map_err(|err| { + error!("Error committing transaction: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + Ok(Json(CreateCharacterResponse { + success: true, + inference_job_token: apriori_job_token, + })) +} + +// =============== Private helpers =============== + +/// Upload an image from our CDN to Kinovi's CDN via prepare + upload. +async fn upload_to_kinovi( + cookies: &str, + our_cdn_url: &url::Url, +) -> Result { + let extension = extract_extension_from_url(our_cdn_url, &ExtractExtensions::All) + .map(|ext| ext.without_period().to_string()) + .unwrap_or_else(|| "png".to_string()); + + let session = Seedance2ProSession::from_cookies_string(cookies.to_string()); + + let file_bytes = http_download_url_to_bytes(our_cdn_url.as_str()) + .await + .map_err(|err| { + warn!("Error downloading media file from CDN: {:?}", err); + AdvancedCommonWebError::from_anyhow_error(err) + })? + .to_vec(); + + let prepare_result = prepare_file_upload(PrepareFileUploadArgs { + session: &session, + extension, + host_override: None, + }) + .await + .map_err(|err| { + warn!("Error preparing Kinovi file upload: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + let upload_result = upload_file(UploadFileArgs { + upload_url: prepare_result.upload_url, + file_bytes, + host_override: None, + }) + .await + .map_err(|err| { + warn!("Error uploading file to Kinovi: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + Ok(upload_result.public_url) +} + +fn truncate_string(s: &str, max_len: usize) -> String { + if s.len() <= max_len { + s.to_string() + } else { + s[..max_len].to_string() + } } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/delete_character_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/delete_character_handler.rs index 6ef15cccc8..23bcff002f 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/delete_character_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/delete_character_handler.rs @@ -2,11 +2,15 @@ use std::sync::Arc; use actix_web::web::{Json, Path}; use actix_web::{web, HttpRequest}; +use log::{info, warn}; use artcraft_api_defs::characters::delete_character::{DeleteCharacterPathInfo, DeleteCharacterResponse}; use tokens::tokens::characters::CharacterToken; +use mysql_queries::queries::characters::delete_character::delete_character; +use mysql_queries::queries::characters::get_character_by_token_including_deleted::get_character_by_token_including_deleted; -use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection; use crate::state::server_state::ServerState; /// Delete a character. @@ -25,9 +29,61 @@ use crate::state::server_state::ServerState; ) )] pub async fn delete_character_handler( - _http_request: HttpRequest, - _path: Path, - _server_state: web::Data>, -) -> Result, CommonWebError> { - todo!() + http_request: HttpRequest, + path: Path, + server_state: web::Data>, +) -> Result, AdvancedCommonWebError> { + + let character_token = &path.character_token; + + // --- Auth --- + + let mut mysql_connection = server_state.mysql_pool.acquire().await?; + + let user_session = require_user_session_using_connection( + &http_request, + &server_state.session_checker, + &mut mysql_connection, + ).await?; + + let user_token = &user_session.user_token; + let is_mod = user_session.is_mod(); + + // --- Look up the character (including deleted) --- + + let character = get_character_by_token_including_deleted(character_token, &mut mysql_connection) + .await? + .ok_or_else(|| { + warn!("Character not found: {}", character_token); + AdvancedCommonWebError::NotFound + })?; + + // Already deleted — return success idempotently. + if character.is_deleted { + info!("Character {} is already deleted", character_token); + return Ok(Json(DeleteCharacterResponse { success: true })); + } + + // --- Ownership check --- + + let is_owner = character.maybe_creator_user_token + .as_ref() + .map(|owner| owner == user_token) + .unwrap_or(false); + + if !is_owner && !is_mod { + warn!( + "User {} attempted to delete character {} owned by {:?}", + user_token, character_token, character.maybe_creator_user_token, + ); + return Err(AdvancedCommonWebError::NotAuthorized); + } + + // --- Soft delete --- + + delete_character(character_token, &mut mysql_connection).await?; + + info!("Character {} deleted by user {}", character_token, user_token); + + Ok(Json(DeleteCharacterResponse { success: true })) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/edit_character_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/edit_character_handler.rs index 709279938d..8f93b83ce4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/edit_character_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/edit_character_handler.rs @@ -2,10 +2,16 @@ use std::sync::Arc; use actix_web::web::Json; use actix_web::{web, HttpRequest}; +use log::{error, info, warn}; use artcraft_api_defs::characters::edit_character::{EditCharacterRequest, EditCharacterResponse}; +use mysql_queries::queries::characters::get_character_by_token::get_character_by_token; +use mysql_queries::queries::characters::update_character_name_and_description::update_character_name_and_description; +use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession; +use seedance2pro_client::requests::update_character::update_character::{update_character, UpdateCharacterArgs}; -use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection; use crate::state::server_state::ServerState; /// Edit a character's name or description. @@ -23,9 +29,151 @@ use crate::state::server_state::ServerState; ), )] pub async fn edit_character_handler( - _http_request: HttpRequest, - _request: Json, - _server_state: web::Data>, -) -> Result, CommonWebError> { - todo!() + http_request: HttpRequest, + request: Json, + server_state: web::Data>, +) -> Result, AdvancedCommonWebError> { + + // --- Auth --- + + let mut mysql_connection = server_state.mysql_pool.acquire().await?; + + let user_session = require_user_session_using_connection( + &http_request, + &server_state.session_checker, + &mut mysql_connection, + ).await?; + + let user_token = &user_session.user_token; + let is_mod = user_session.is_mod(); + + // --- Look up character --- + + let character = get_character_by_token(&request.token, &mut mysql_connection) + .await? + .ok_or_else(|| { + warn!("Character not found: {}", request.token); + AdvancedCommonWebError::NotFound + })?; + + // --- Ownership check --- + + let is_owner = character.maybe_creator_user_token + .as_ref() + .map(|owner| owner == user_token) + .unwrap_or(false); + + if !is_owner && !is_mod { + warn!("User {} tried to edit character {} they don't own", user_token, request.token); + return Err(AdvancedCommonWebError::NotFound); + } + + // --- Determine what to update --- + + let new_name = resolve_name_update(&request.updated_name); + let description_update = resolve_description_update(&request.updated_description); + + let has_name_change = new_name.is_some(); + let has_description_change = !matches!(description_update, DescriptionUpdateType::NoUpdate); + + if !has_name_change && !has_description_change { + return Ok(Json(EditCharacterResponse { success: true })); + } + + // --- Compute final values --- + + let final_name = new_name.unwrap_or_else(|| character.character_name.clone().unwrap_or_default()); + + let final_kinovi_name = if has_name_change { final_name.clone() } else { + character.kinovi_character_name.clone() + .or_else(|| character.character_name.clone()) + .unwrap_or_default() + }; + + let final_description = match &description_update { + DescriptionUpdateType::NoUpdate => character.maybe_description.clone(), + DescriptionUpdateType::Nullify => None, + DescriptionUpdateType::Update(text) => Some(text.clone()), + }; + + // --- If renaming, update Kinovi first --- + + if has_name_change { + if let Some(ref kinovi_id) = character.kinovi_character_id { + let session = Seedance2ProSession::from_cookies_string( + server_state.seedance2pro.cookies.clone() + ); + + update_character(UpdateCharacterArgs { + session: &session, + character_id: kinovi_id.clone(), + name: final_name.clone(), + description: final_description.clone().unwrap_or_default(), + host_override: None, + }) + .await + .map_err(|err| { + error!("Error updating character on Kinovi: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + info!("Updated character {} on Kinovi (name='{}')", kinovi_id, final_name); + } + } + + // --- Update database --- + + update_character_name_and_description( + &request.token, + &final_name, + &final_kinovi_name, + final_description.as_deref(), + &mut mysql_connection, + ).await?; + + info!("Updated character {} in database", request.token); + + Ok(Json(EditCharacterResponse { success: true })) +} + +// =============== Private helpers =============== + +const MAX_NAME_LENGTH: usize = 50; +const MAX_DESCRIPTION_LENGTH: usize = 500; + +enum DescriptionUpdateType { + NoUpdate, + Nullify, + Update(String), +} + +/// Determine the new name, if any. +/// Returns None if the name should not be updated. +fn resolve_name_update(updated_name: &Option) -> Option { + let name = updated_name.as_ref()?; + let trimmed = name.trim(); + if trimmed.is_empty() { + None + } else { + Some(truncate(trimmed, MAX_NAME_LENGTH)) + } +} + +/// Determine the description update type. +fn resolve_description_update(updated_description: &Option) -> DescriptionUpdateType { + let desc = match updated_description.as_ref() { + None => return DescriptionUpdateType::NoUpdate, + Some(d) => d, + }; + + let trimmed = desc.trim(); + if trimmed.is_empty() { + DescriptionUpdateType::Nullify + } else { + DescriptionUpdateType::Update(truncate(trimmed, MAX_DESCRIPTION_LENGTH)) + } +} + +fn truncate(s: &str, max_len: usize) -> String { + if s.len() <= max_len { s.to_string() } else { s[..max_len].to_string() } } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/get_character_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/get_character_handler.rs index 49483b113f..0454dc8bde 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/get_character_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/get_character_handler.rs @@ -5,14 +5,17 @@ use actix_web::{web, HttpRequest}; use log::warn; use artcraft_api_defs::characters::get_character::{GetCharacterDetails, GetCharacterPathInfo, GetCharacterResponse}; +use artcraft_api_defs::common::responses::media_links::MediaLinks; use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; use enums::common::generation::common_model_type::CommonModelType; -use mysql_queries::queries::media_files::get::get_media_file::get_media_file; +use mysql_queries::queries::characters::get_character_by_token::get_character_by_token; +use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens_with_connection, MediaFilesByTokensRecord}; use tokens::tokens::characters::CharacterToken; use tokens::tokens::media_files::MediaFileToken; +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::common_responses::media::media_domain::MediaDomain; use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder; -use crate::http_server::common_responses::common_web_error::CommonWebError; use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; use crate::state::server_state::ServerState; @@ -34,42 +37,67 @@ pub async fn get_character_handler( http_request: HttpRequest, path: Path, server_state: web::Data>, -) -> Result, CommonWebError> { - // TODO: Replace with real character query. This is test data using a real media file (Ernest). +) -> Result, AdvancedCommonWebError> { + + let mut mysql_connection = server_state.mysql_pool.acquire().await?; + + let character = get_character_by_token(&path.character_token, &mut mysql_connection) + .await? + .ok_or_else(|| { + warn!("Character not found: {}", path.character_token); + AdvancedCommonWebError::NotFound + })?; + + // --- Resolve media links --- let media_domain = get_media_domain(&http_request); - let ernest_media_token = MediaFileToken::new_from_str("m_1997908e4tcbaj34wa0s5bvr4y4szt"); - let maybe_links = match get_media_file(&ernest_media_token, false, &server_state.mysql_pool).await { - Ok(Some(record)) => { - let path = MediaFileBucketPath::from_object_hash( - &record.public_bucket_directory_hash, - record.maybe_public_bucket_prefix.as_deref(), - record.maybe_public_bucket_extension.as_deref(), - ); - Some(MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &path)) - } - Ok(None) => { - warn!("Ernest media file not found in database"); - None - } - Err(err) => { - warn!("Failed to look up Ernest media file: {:?}", err); - None - } + let mut media_tokens_to_lookup: Vec = Vec::new(); + if let Some(ref t) = character.maybe_avatar_media_token { media_tokens_to_lookup.push(t.clone()); } + if let Some(ref t) = character.maybe_full_image_media_token { media_tokens_to_lookup.push(t.clone()); } + if let Some(ref t) = character.maybe_original_upload_media_token { media_tokens_to_lookup.push(t.clone()); } + + let media_records = if media_tokens_to_lookup.is_empty() { + vec![] + } else { + batch_get_media_files_by_tokens_with_connection(&mut mysql_connection, &media_tokens_to_lookup, true) + .await + .unwrap_or_else(|err| { + warn!("Failed to look up media files for character: {:?}", err); + vec![] + }) }; - let character = GetCharacterDetails { - token: path.character_token.clone(), - models: vec![CommonModelType::Seedance2p0], - name: "Ernest".to_string(), - maybe_description: Some("Ernest P. Worrell".to_string()), - maybe_avatar: maybe_links.clone(), - maybe_full_image: maybe_links, - }; + let maybe_avatar = resolve_media_links(&media_records, &character.maybe_avatar_media_token, media_domain, server_state.server_environment); + let maybe_full_image = resolve_media_links(&media_records, &character.maybe_full_image_media_token, media_domain, server_state.server_environment); Ok(Json(GetCharacterResponse { success: true, - character, + character: GetCharacterDetails { + token: character.token, + models: vec![CommonModelType::Seedance2p0], + name: character.character_name.unwrap_or_default(), + maybe_description: character.maybe_description, + maybe_avatar, + maybe_full_image, + }, })) } + +// =============== Private helpers =============== + +fn resolve_media_links( + media_records: &[MediaFilesByTokensRecord], + maybe_token: &Option, + media_domain: MediaDomain, + server_environment: server_environment::ServerEnvironment, +) -> Option { + let token = maybe_token.as_ref()?; + let record = media_records.iter().find(|r| &r.token == token)?; + let path = MediaFileBucketPath::from_object_hash( + &record.public_bucket_directory_hash, + record.maybe_public_bucket_prefix.as_deref(), + record.maybe_public_bucket_extension.as_deref(), + ); + Some(MediaLinksBuilder::from_media_path_and_env(media_domain, server_environment, &path)) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/list_characters_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/list_characters_handler.rs index 4d66226bb6..463790e45c 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/list_characters_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/list_characters_handler.rs @@ -5,15 +5,18 @@ use actix_web::{web, HttpRequest}; use log::warn; use artcraft_api_defs::characters::list_characters::{ListCharactersEntry, ListCharactersQuery, ListCharactersResponse}; +use artcraft_api_defs::common::responses::media_links::MediaLinks; use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; use enums::common::generation::common_model_type::CommonModelType; -use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens; -use tokens::tokens::characters::CharacterToken; +use mysql_queries::queries::characters::list_active_characters_for_user::list_active_characters_for_user; +use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens_with_connection, MediaFilesByTokensRecord}; use tokens::tokens::media_files::MediaFileToken; +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::common_responses::media::media_domain::MediaDomain; use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder; -use crate::http_server::common_responses::common_web_error::CommonWebError; use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection; use crate::state::server_state::ServerState; /// List characters for the current session. @@ -30,62 +33,92 @@ use crate::state::server_state::ServerState; )] pub async fn list_characters_handler( http_request: HttpRequest, - _query: Query, + query: Query, server_state: web::Data>, -) -> Result, CommonWebError> { - // TODO: Replace with real character queries. This is test data using real media files. +) -> Result, AdvancedCommonWebError> { + + let mut mysql_connection = server_state.mysql_pool.acquire().await?; + + let user_session = require_user_session_using_connection( + &http_request, + &server_state.session_checker, + &mut mysql_connection, + ).await?; + + let user_token = &user_session.user_token; + + // --- Query characters --- + + let result = list_active_characters_for_user(user_token, query.cursor, &mut mysql_connection) + .await?; + + if result.characters.is_empty() { + return Ok(Json(ListCharactersResponse { + success: true, + characters: vec![], + next_cursor: None, + })); + } + + // --- Resolve media links --- let media_domain = get_media_domain(&http_request); - struct TestCharacter { - token: &'static str, - name: &'static str, - description: &'static str, - media_token: &'static str, + let mut media_tokens_to_lookup: Vec = Vec::new(); + for character in &result.characters { + if let Some(ref t) = character.maybe_avatar_media_token { media_tokens_to_lookup.push(t.clone()); } + if let Some(ref t) = character.maybe_full_image_media_token { media_tokens_to_lookup.push(t.clone()); } + if let Some(ref t) = character.maybe_original_upload_media_token { media_tokens_to_lookup.push(t.clone()); } } - let test_characters = vec![ - TestCharacter { token: "character_mock_ernest_001", name: "Ernest", description: "Ernest P. Worrell", media_token: "m_1997908e4tcbaj34wa0s5bvr4y4szt" }, - TestCharacter { token: "character_mock_juno_002", name: "Juno", description: "Juno the corgi", media_token: "m_m1bz02z1kkzanxy6rb4vk1kvq9de9g" }, - TestCharacter { token: "character_mock_mochi_003", name: "Mochi", description: "Mochi with glasses", media_token: "m_5y81rcnpnrbey55da6cc2s9z4d2f45" }, - ]; + let media_records = if media_tokens_to_lookup.is_empty() { + vec![] + } else { + batch_get_media_files_by_tokens_with_connection(&mut mysql_connection, &media_tokens_to_lookup, false) + .await + .unwrap_or_else(|err| { + warn!("Failed to look up media files for characters: {:?}", err); + vec![] + }) + }; - let media_tokens: Vec = test_characters.iter() - .map(|c| MediaFileToken::new_from_str(c.media_token)) - .collect(); + // --- Build response --- - let media_records = batch_get_media_files_by_tokens(&server_state.mysql_pool, &media_tokens, false) - .await - .unwrap_or_else(|err| { - warn!("Failed to look up media files for test characters: {:?}", err); - vec![] - }); - - let characters = test_characters.iter().map(|tc| { - let maybe_links = media_records.iter() - .find(|r| r.token.as_str() == tc.media_token) - .map(|r| { - let path = MediaFileBucketPath::from_object_hash( - &r.public_bucket_directory_hash, - r.maybe_public_bucket_prefix.as_deref(), - r.maybe_public_bucket_extension.as_deref(), - ); - MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &path) - }); + let characters = result.characters.iter().map(|c| { + let maybe_avatar = resolve_media_links(&media_records, &c.maybe_avatar_media_token, media_domain, server_state.server_environment); + let maybe_full_image = resolve_media_links(&media_records, &c.maybe_full_image_media_token, media_domain, server_state.server_environment); ListCharactersEntry { - token: CharacterToken::new_from_str(tc.token), + token: c.token.clone(), models: vec![CommonModelType::Seedance2p0], - name: tc.name.to_string(), - maybe_description: Some(tc.description.to_string()), - maybe_avatar: maybe_links.clone(), - maybe_full_image: maybe_links, + name: c.character_name.clone().unwrap_or_default(), + maybe_description: c.maybe_description.clone(), + maybe_avatar, + maybe_full_image, } }).collect(); Ok(Json(ListCharactersResponse { success: true, characters, - next_cursor: None, + next_cursor: result.next_cursor, })) } + +// =============== Private helpers =============== + +fn resolve_media_links( + media_records: &[MediaFilesByTokensRecord], + maybe_token: &Option, + media_domain: MediaDomain, + server_environment: server_environment::ServerEnvironment, +) -> Option { + let token = maybe_token.as_ref()?; + let record = media_records.iter().find(|r| &r.token == token)?; + let path = MediaFileBucketPath::from_object_hash( + &record.public_bucket_directory_hash, + record.maybe_public_bucket_prefix.as_deref(), + record.maybe_public_bucket_extension.as_deref(), + ); + Some(MediaLinksBuilder::from_media_path_and_env(media_domain, server_environment, &path)) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs index 5fc61add77..b14260a604 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs @@ -25,6 +25,7 @@ use enums::common::generation_provider::GenerationProvider; use enums::common::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use log::{error, info, warn}; +use mysql_queries::queries::characters::batch_lookup_characters_by_token_for_prompting::batch_lookup_characters_by_token_for_prompting; use mysql_queries::queries::generic_inference::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::{ insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token, InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs, @@ -45,6 +46,8 @@ use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{ }; use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs}; use sqlx::Acquire; +use sqlx::MySql; +use tokens::tokens::characters::CharacterToken; use tokens::tokens::generic_inference_jobs::InferenceJobToken; use tokens::tokens::media_files::MediaFileToken; use url::Url; @@ -142,6 +145,13 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( ).await? }; + // --- Look up character tokens (if any) --- + + let kinovi_character_ids = resolve_kinovi_character_ids( + request.reference_character_tokens.as_deref(), + &mut mysql_connection, + ).await?; + // --- Insert idempotency token --- insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection) @@ -191,6 +201,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( resolution, batch_count, duration_seconds, + kinovi_character_ids.clone(), ).await; match result { @@ -212,6 +223,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( resolution, batch_count, duration_seconds, + kinovi_character_ids.clone(), ).await; match result { @@ -248,6 +260,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( resolution, batch_count, duration_seconds, + kinovi_character_ids, ).await; match result { @@ -480,6 +493,7 @@ fn estimate_cost_upfront(resolution: Resolution, batch_count: BatchCount, durati reference_image_urls: None, reference_video_urls: None, reference_audio_urls: None, + character_ids: None, use_face_blur_hack: None, host_override: None, }; @@ -495,6 +509,7 @@ async fn upload_and_generate( resolution: Resolution, batch_count: BatchCount, duration_seconds: u8, + kinovi_character_ids: Option>, ) -> Result { // --- Upload files to seedance2pro CDN --- @@ -569,6 +584,7 @@ async fn upload_and_generate( reference_image_urls, reference_video_urls, reference_audio_urls, + character_ids: kinovi_character_ids, use_face_blur_hack: None, host_override: None, }; @@ -585,6 +601,44 @@ async fn upload_and_generate( }) } +/// Resolve character tokens to Kinovi character IDs for prompting. +/// +/// Looks up the characters, filters to active ones with kinovi IDs, and warns about +/// any that are missing or inactive (but doesn't fail the request). +async fn resolve_kinovi_character_ids( + maybe_tokens: Option<&[CharacterToken]>, + connection: &mut sqlx::pool::PoolConnection, +) -> Result>, AdvancedCommonWebError> { + let tokens = match maybe_tokens { + None => return Ok(None), + Some(tokens) if tokens.is_empty() => return Ok(None), + Some(tokens) => tokens, + }; + + let characters = batch_lookup_characters_by_token_for_prompting(tokens, connection) + .await?; + + if characters.len() != tokens.len() { + warn!( + "Not all character tokens were found: requested {}, found {}", + tokens.len(), characters.len(), + ); + } + + for character in &characters { + if !character.is_active { + warn!("Character {} is not yet active, skipping", character.token); + } + } + + let ids: Vec = characters.iter() + .filter(|c| c.is_active) + .filter_map(|c| c.kinovi_character_id.clone()) + .collect(); + + if ids.is_empty() { Ok(None) } else { Ok(Some(ids)) } +} + /// Uploads a list of reference media tokens to seedance2pro, returning the resulting URLs. /// Returns `None` if the token list is absent or empty. async fn upload_reference_tokens_to_seedance2pro( diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/estimates/estimate_job_progress.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/estimates/estimate_job_progress.rs index 2365e439d5..e829cd0ee4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/estimates/estimate_job_progress.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/estimates/estimate_job_progress.rs @@ -85,6 +85,7 @@ pub fn estimate_job_progress(job: &GenericInferenceJobStatus, maybe_args: Option InferenceCategory::Mocap => 0, InferenceCategory::VideoFilter => 0, InferenceCategory::ConvertBvhToWorkflow => 0, + InferenceCategory::CharacterGeneration => percent(duration_seconds, 60*2), InferenceCategory::SplatGeneration => percent(duration_seconds, 60*5), InferenceCategory::DeprecatedField => 0, // TODO(bt,2024-07-16): Read job type instead. }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ecf071c06d..75cf29282e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -346,6 +346,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -361,6 +362,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -816,6 +818,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1307,7 +1310,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1319,7 +1321,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1331,7 +1332,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -1343,7 +1343,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1405,7 +1404,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1417,7 +1415,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1444,7 +1441,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1456,7 +1452,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1468,7 +1463,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1480,7 +1474,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1492,7 +1485,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1504,7 +1496,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1516,7 +1507,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1531,7 +1521,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -2751,6 +2740,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -2824,6 +2814,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2968,6 +2959,7 @@ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", "license": "MIT", + "peer": true, "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -2997,7 +2989,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=12" } @@ -3015,7 +3006,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -3033,7 +3023,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -3051,7 +3040,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=12" } @@ -3069,7 +3057,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=12" } @@ -3087,7 +3074,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=12" } @@ -3105,7 +3091,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -3123,7 +3108,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -3141,7 +3125,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3159,7 +3142,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3177,7 +3159,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3195,7 +3176,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3213,7 +3193,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3231,7 +3210,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3249,7 +3227,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3267,7 +3244,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3285,7 +3261,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=12" } @@ -3319,7 +3294,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -3353,7 +3327,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=12" } @@ -3387,7 +3360,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=12" } @@ -3405,7 +3377,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -3423,7 +3394,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -3441,7 +3411,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=12" } @@ -3669,6 +3638,7 @@ "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" }, @@ -4037,7 +4007,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", - "peer": true, "engines": { "node": ">=12" } @@ -4046,7 +4015,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "peer": true, "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -4062,7 +4030,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "peer": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -4071,7 +4038,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "peer": true, "engines": { "node": ">=6" } @@ -4080,7 +4046,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -4093,7 +4058,6 @@ "version": "3.14.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "peer": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4106,7 +4070,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -4118,7 +4081,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -4133,7 +4095,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -4145,7 +4106,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "peer": true, "engines": { "node": ">=8" } @@ -4163,7 +4123,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3" }, @@ -4175,7 +4134,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "peer": true, "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -4203,7 +4161,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -4259,6 +4216,7 @@ "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "license": "MIT", + "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -5759,6 +5717,7 @@ "integrity": "sha512-fnP+ZOZTFeBGiTAnxve+axGmiYn2D60h86nUISXjXClK3LUY1krUfPgf6MaD4YDJ4i51OGXZWPekeMe16pkd8Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime": "0.21.6", "@module-federation/webpack-bundler-runtime": "0.21.6" @@ -5952,6 +5911,7 @@ "integrity": "sha512-kzFn3ObUeBp5vaEtN1WMxhTYBuYEErxugu1RzFUERD21X3BZ+b4cWwdFJuBDlsmVjctIg/QSOoZoPXRKAO0foA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime": "0.15.0", "@module-federation/webpack-bundler-runtime": "0.15.0" @@ -6066,6 +6026,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -8433,7 +8394,6 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.84.1.tgz", "integrity": "sha512-lAJ6PDZv95FdT9s9uhc9ivhikW1Zwh4j9XdXM7J2l4oUA3t37qfoBmTSDLuPyE3Bi+Xtwa11hJm0BUTT2sc/gg==", - "peer": true, "engines": { "node": ">= 20.19.4" } @@ -8442,7 +8402,6 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.84.1.tgz", "integrity": "sha512-n1RIU0QAavgCg1uC5+s53arL7/mpM+16IBhJ3nCFSd/iK5tUmCwxQDcIDC703fuXfpub/ZygeSjVN8bcOWn0gA==", - "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", @@ -8463,7 +8422,6 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.84.1.tgz", "integrity": "sha512-f6a+mJEJ6Joxlt/050TqYUr7uRRbeKnz8lnpL7JajhpsgZLEbkJRjH8HY5QiLcRdUwWFtizml4V+vcO3P4RxoQ==", - "peer": true, "dependencies": { "@react-native/dev-middleware": "0.84.1", "debug": "^4.4.0", @@ -8493,7 +8451,6 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.84.1.tgz", "integrity": "sha512-rUU/Pyh3R5zT0WkVgB+yA6VwOp7HM5Hz4NYE97ajFS07OUIcv8JzBL3MXVdSSjLfldfqOuPEuKUaZcAOwPgabw==", - "peer": true, "engines": { "node": ">= 20.19.4" } @@ -8502,7 +8459,6 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.84.1.tgz", "integrity": "sha512-LIGhh4q4ette3yW5OzmukNMYwmINYrRGDZqKyTYc/VZyNpblZPw72coXVHXdfpPT6+YlxHqXzn3UjFZpNODGCQ==", - "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "debug": "^4.4.0", @@ -8516,7 +8472,6 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.84.1.tgz", "integrity": "sha512-Z83ra+Gk6ElAhH3XRrv3vwbwCPTb04sPPlNpotxcFZb5LtRQZwT91ZQEXw3GOJCVIFp9EQ/gj8AQbVvtHKOUlQ==", - "peer": true, "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.84.1", @@ -8539,7 +8494,6 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "peer": true, "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -8555,7 +8509,6 @@ "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "peer": true, "engines": { "node": ">=8.3.0" }, @@ -8576,7 +8529,6 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.84.1.tgz", "integrity": "sha512-7uVlPBE3uluRNRX4MW7PUJIO1LDBTpAqStKHU7LHH+GRrdZbHsWtOEAX8PiY4GFfBEvG8hEjiuTOqAxMjV+hDg==", - "peer": true, "engines": { "node": ">= 20.19.4" } @@ -8585,7 +8537,6 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.84.1.tgz", "integrity": "sha512-UsTe2AbUugsfyI7XIHMQq4E7xeC8a6GrYwuK+NohMMMJMxmyM3JkzIk+GB9e2il6ScEQNMJNaj+q+i5za8itxQ==", - "peer": true, "engines": { "node": ">= 20.19.4" } @@ -8593,8 +8544,7 @@ "node_modules/@react-native/normalize-colors": { "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.84.1.tgz", - "integrity": "sha512-/UPaQ4jl95soXnLDEJ6Cs6lnRXhwbxtT4KbZz+AFDees7prMV2NOLcHfCnzmTabf5Y3oxENMVBL666n4GMLcTA==", - "peer": true + "integrity": "sha512-/UPaQ4jl95soXnLDEJ6Cs6lnRXhwbxtT4KbZz+AFDees7prMV2NOLcHfCnzmTabf5Y3oxENMVBL666n4GMLcTA==" }, "node_modules/@react-oauth/google": { "version": "0.13.4", @@ -8752,6 +8702,7 @@ "integrity": "sha512-gfc4Hu2Sysr+GyU/F12d2uvEfQwUwWvsOjBtiemhdN1xGOn1+FyYzlLl/aJ7AL9oYko8sDqOPyJCiApWJJGCCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.8", "@babel/generator": "^7.21.5", @@ -9580,6 +9531,7 @@ "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.17.2.tgz", "integrity": "sha512-dTrAG1SgOLgz1DFBDsLHN0V34YqLsHEReVHYOI4UV/p+ALbn/BRQMw1MaUuqGXmX21ZTuCzzPegtTLNEOc8ixA==", "license": "MIT", + "peer": true, "dependencies": { "@remix-run/router": "1.23.0", "@types/cookie": "^0.6.0", @@ -10172,6 +10124,7 @@ "integrity": "sha512-2mR+2YBydlgZ7Q0Rpd6bCC3MBnV9TS0x857K0zIhbDj4BQOqaWVy1n7fx/B3MrS8TR0QCuzKfyDAjNz+XTyJVQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.21.6", "@rspack/binding": "1.6.6", @@ -10484,7 +10437,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "peer": true, "dependencies": { "type-detect": "4.0.8" } @@ -10493,7 +10445,6 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -10900,6 +10851,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -11082,6 +11034,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.8" @@ -11296,6 +11249,7 @@ "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3" } @@ -11637,6 +11591,7 @@ "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -11987,7 +11942,6 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "peer": true, "dependencies": { "@types/node": "*" } @@ -12137,6 +12091,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -12159,6 +12114,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz", "integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -12497,6 +12453,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -14706,6 +14663,7 @@ "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz", "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -14767,6 +14725,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -14849,6 +14808,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14921,8 +14881,7 @@ "node_modules/anser": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", - "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", - "peer": true + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==" }, "node_modules/ansi-colors": { "version": "4.1.3", @@ -15214,8 +15173,7 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "peer": true + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, "node_modules/assertion-error": { "version": "2.0.1", @@ -15441,7 +15399,6 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -15457,7 +15414,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -15468,7 +15424,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -15488,7 +15443,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -15500,7 +15454,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "peer": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -15514,7 +15467,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "peer": true, "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -15624,7 +15576,6 @@ "version": "0.32.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", - "peer": true, "dependencies": { "hermes-parser": "0.32.0" } @@ -15643,7 +15594,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "peer": true, "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -15669,7 +15619,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "peer": true, "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -15886,6 +15835,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -15917,7 +15867,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "peer": true, "dependencies": { "node-int64": "^0.4.0" } @@ -16318,7 +16267,6 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", - "peer": true, "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", @@ -16346,7 +16294,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", - "peer": true, "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", @@ -16754,7 +16701,6 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "peer": true, "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", @@ -16769,7 +16715,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -16778,7 +16723,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -16787,7 +16731,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -16804,14 +16747,12 @@ "node_modules/connect/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/connect/node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -16823,7 +16764,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "peer": true, "engines": { "node": ">= 0.6" } @@ -17159,7 +17099,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -17862,7 +17803,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -17918,6 +17860,7 @@ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -18009,7 +17952,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "peer": true, "dependencies": { "stackframe": "^1.3.4" } @@ -18221,6 +18163,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -18645,6 +18588,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -18701,6 +18645,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -18802,6 +18747,7 @@ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -19536,14 +19482,14 @@ "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "peer": true + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==" }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -19715,7 +19661,6 @@ "version": "0.5.8", "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", "integrity": "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==", - "peer": true, "bin": { "dotslash": "bin/dotslash" }, @@ -19727,7 +19672,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "peer": true, "dependencies": { "bser": "2.1.1" } @@ -19841,6 +19785,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -20033,8 +19978,7 @@ "node_modules/flow-enums-runtime": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", - "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", - "peer": true + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==" }, "node_modules/follow-redirects": { "version": "1.15.11", @@ -20438,7 +20382,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -20912,20 +20855,17 @@ "node_modules/hermes-compiler": { "version": "250829098.0.9", "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.9.tgz", - "integrity": "sha512-hZ5O7PDz1vQ99TS7HD3FJ9zVynfU1y+VWId6U1Pldvd8hmAYrNec/XLPYJKD3dLOW6NXak6aAQAuMuSo3ji0tQ==", - "peer": true + "integrity": "sha512-hZ5O7PDz1vQ99TS7HD3FJ9zVynfU1y+VWId6U1Pldvd8hmAYrNec/XLPYJKD3dLOW6NXak6aAQAuMuSo3ji0tQ==" }, "node_modules/hermes-estree": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", - "peer": true + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==" }, "node_modules/hermes-parser": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", - "peer": true, "dependencies": { "hermes-estree": "0.32.0" } @@ -20947,6 +20887,7 @@ "version": "4.12.7", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -21263,7 +21204,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "peer": true, "dependencies": { "queue": "6.0.2" }, @@ -22154,7 +22094,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "peer": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -22170,7 +22109,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "peer": true, "bin": { "semver": "bin/semver.js" } @@ -22341,7 +22279,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -22367,7 +22304,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -22495,7 +22431,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -22509,7 +22444,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "peer": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -22519,6 +22453,7 @@ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "license": "MIT", + "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -22547,7 +22482,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -22564,7 +22498,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "peer": true, "engines": { "node": ">=10" }, @@ -22576,7 +22509,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -22589,14 +22521,12 @@ "node_modules/jest-validate/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "peer": true + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/jest-worker": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "peer": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -22611,7 +22541,6 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -22668,8 +22597,7 @@ "node_modules/jsc-safe-url": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", - "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", - "peer": true + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==" }, "node_modules/jsdom": { "version": "22.1.0", @@ -22677,6 +22605,7 @@ "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "abab": "^2.0.6", "cssstyle": "^3.0.0", @@ -22986,7 +22915,8 @@ "url": "https://github.com/sponsors/lavrton" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/language-subtag-registry": { "version": "0.3.23", @@ -23012,7 +22942,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "peer": true, "engines": { "node": ">=6" } @@ -23035,7 +22964,6 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", - "peer": true, "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" @@ -23045,7 +22973,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -23053,13 +22980,13 @@ "node_modules/lighthouse-logger/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -23726,8 +23653,7 @@ "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", - "peer": true + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" }, "node_modules/log-symbols": { "version": "4.1.0", @@ -24049,7 +23975,6 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "peer": true, "dependencies": { "tmpl": "1.0.5" } @@ -24067,8 +23992,7 @@ "node_modules/marky": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", - "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", - "peer": true + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==" }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -24315,8 +24239,7 @@ "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "peer": true + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, "node_modules/merge-descriptors": { "version": "1.0.3", @@ -24362,7 +24285,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.5.tgz", "integrity": "sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ==", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", @@ -24416,7 +24338,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.5.tgz", "integrity": "sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA==", - "peer": true, "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", @@ -24430,14 +24351,12 @@ "node_modules/metro-babel-transformer/node_modules/hermes-estree": { "version": "0.33.3", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "peer": true + "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==" }, "node_modules/metro-babel-transformer/node_modules/hermes-parser": { "version": "0.33.3", "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "peer": true, "dependencies": { "hermes-estree": "0.33.3" } @@ -24446,7 +24365,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.5.tgz", "integrity": "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng==", - "peer": true, "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", @@ -24461,7 +24379,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.5.tgz", "integrity": "sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" }, @@ -24473,7 +24390,6 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "peer": true, "engines": { "node": ">= 14" } @@ -24482,7 +24398,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -24495,7 +24410,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.5.tgz", "integrity": "sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w==", - "peer": true, "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", @@ -24514,7 +24428,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.5.tgz", "integrity": "sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", @@ -24528,7 +24441,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.5.tgz", "integrity": "sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ==", - "peer": true, "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", @@ -24548,7 +24460,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.5.tgz", "integrity": "sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" @@ -24561,7 +24472,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.5.tgz", "integrity": "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" }, @@ -24573,7 +24483,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.5.tgz", "integrity": "sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" @@ -24586,7 +24495,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.5.tgz", "integrity": "sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ==", - "peer": true, "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", @@ -24606,7 +24514,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -24615,7 +24522,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.5.tgz", "integrity": "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", @@ -24635,7 +24541,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -24644,7 +24549,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.5.tgz", "integrity": "sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw==", - "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", @@ -24661,7 +24565,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.5.tgz", "integrity": "sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA==", - "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", @@ -24685,7 +24588,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -24697,20 +24599,17 @@ "node_modules/metro/node_modules/ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "peer": true + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" }, "node_modules/metro/node_modules/hermes-estree": { "version": "0.33.3", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "peer": true + "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==" }, "node_modules/metro/node_modules/hermes-parser": { "version": "0.33.3", "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "peer": true, "dependencies": { "hermes-estree": "0.33.3" } @@ -24719,7 +24618,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "peer": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -24735,7 +24633,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "peer": true, "engines": { "node": ">= 0.6" } @@ -24744,7 +24641,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -24753,7 +24649,6 @@ "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "peer": true, "engines": { "node": ">=8.3.0" }, @@ -26081,8 +25976,7 @@ "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "peer": true + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node_modules/node-machine-id": { "version": "1.1.12", @@ -26265,8 +26159,7 @@ "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "peer": true + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" }, "node_modules/nwsapi": { "version": "2.2.22", @@ -26281,6 +26174,7 @@ "integrity": "sha512-2wL/2fSmIbRWn6zXaQ/g3kj5DfEaTw/aJkPr6ozJh8BUq5iYKE+tS9oh0PjsVVwN6Pybe80Lu+mn9RgWyeV3xw==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -26437,7 +26331,6 @@ "version": "0.83.5", "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.5.tgz", "integrity": "sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" }, @@ -26823,7 +26716,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "peer": true, "engines": { "node": ">=6" } @@ -27221,6 +27113,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -27526,6 +27419,7 @@ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -27609,6 +27503,7 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -27759,7 +27654,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", - "peer": true, "dependencies": { "asap": "~2.0.6" } @@ -27941,7 +27835,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "peer": true, "dependencies": { "inherits": "~2.0.3" } @@ -28100,6 +27993,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -28147,7 +28041,6 @@ "version": "6.1.5", "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", - "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -28157,7 +28050,6 @@ "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "peer": true, "engines": { "node": ">=8.3.0" }, @@ -28179,6 +28071,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -28531,7 +28424,6 @@ "version": "9.5.0", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", @@ -28579,7 +28471,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", - "peer": true, "dependencies": { "@types/react-reconciler": "^0.28.9" }, @@ -28600,7 +28491,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "peer": true, "engines": { "node": ">=10" }, @@ -28626,7 +28516,6 @@ "url": "https://feross.org/support" } ], - "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -28636,7 +28525,6 @@ "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "peer": true, "engines": { "node": ">=18" } @@ -28645,7 +28533,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -28658,8 +28545,7 @@ "node_modules/react-spring/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "peer": true + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/react-spring/node_modules/react-konva": { "version": "19.2.3", @@ -28679,7 +28565,6 @@ "url": "https://github.com/sponsors/lavrton" } ], - "peer": true, "dependencies": { "@types/react-reconciler": "^0.33.0", "its-fine": "^2.0.0", @@ -28696,7 +28581,6 @@ "version": "0.33.0", "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.33.0.tgz", "integrity": "sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==", - "peer": true, "peerDependencies": { "@types/react": "*" } @@ -28705,7 +28589,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", - "peer": true, "dependencies": { "@types/react-reconciler": "^0.28.9" }, @@ -28717,7 +28600,6 @@ "version": "0.28.9", "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", - "peer": true, "peerDependencies": { "@types/react": "*" } @@ -28726,7 +28608,6 @@ "version": "0.33.0", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -28799,7 +28680,6 @@ "version": "0.84.1", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.84.1.tgz", "integrity": "sha512-sJoDunzhci8ZsqxlUiKoLut4xQeQcmbIgvDHGQKeBz6uEq9HgU+hCWOijMRr6sLP0slQVfBAza34Rq7IbXZZOA==", - "peer": true, "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" @@ -28821,14 +28701,12 @@ "node_modules/react-spring/node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "peer": true + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" }, "node_modules/react-spring/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "peer": true, "engines": { "node": ">=8.3.0" }, @@ -28871,7 +28749,6 @@ "version": "2.1.7", "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "peer": true, "peerDependencies": { "react": ">=16.13", "react-dom": ">=16.13" @@ -29051,8 +28928,7 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "peer": true + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", @@ -29240,8 +29116,7 @@ "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "peer": true + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, "node_modules/resolve": { "version": "1.22.8", @@ -29784,7 +29659,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -30491,7 +30365,6 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -30823,14 +30696,12 @@ "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "peer": true + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, "node_modules/stacktrace-parser": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", - "peer": true, "dependencies": { "type-fest": "^0.7.1" }, @@ -31430,7 +31301,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", - "peer": true, "peerDependencies": { "react": ">=17.0" } @@ -31741,6 +31611,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -31910,13 +31781,13 @@ "version": "0.171.0", "resolved": "https://registry.npmjs.org/three/-/three-0.171.0.tgz", "integrity": "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "peer": true + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" }, "node_modules/through2": { "version": "2.0.5", @@ -32074,8 +31945,7 @@ "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "peer": true + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -32377,7 +32247,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "peer": true, "engines": { "node": ">=4" } @@ -32386,7 +32255,6 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "peer": true, "engines": { "node": ">=8" } @@ -32488,6 +32356,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -33152,6 +33021,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -34045,6 +33915,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -34122,8 +33993,7 @@ "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", - "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", - "peer": true + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==" }, "node_modules/vscode-uri": { "version": "3.1.0", @@ -34149,7 +34019,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "peer": true, "dependencies": { "makeerror": "1.0.12" } @@ -34237,6 +34106,7 @@ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -34337,8 +34207,7 @@ "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "peer": true + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" }, "node_modules/whatwg-mimetype": { "version": "3.0.0", @@ -34621,7 +34490,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "peer": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -34636,6 +34504,7 @@ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -34849,6 +34718,7 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }