feat: add wooden fish play template

This commit is contained in:
2026-05-21 23:34:07 +08:00
parent ef09a23c35
commit 5b0f9f3763
121 changed files with 11580 additions and 159 deletions

View File

@@ -60,6 +60,7 @@ pub fn build_router(state: AppState) -> Router {
.merge(modules::match3d::router(state.clone()))
.merge(modules::square_hole::router(state.clone()))
.merge(modules::jump_hop::router(state.clone()))
.merge(modules::wooden_fish::router(state.clone()))
.merge(modules::puzzle::router(state.clone()))
.merge(visual_novel_router(state.clone()))
.route(

View File

@@ -89,6 +89,7 @@ mod volcengine_speech;
mod wechat_auth;
mod wechat_pay;
mod wechat_provider;
mod wooden_fish;
mod work_author;
mod work_play_tracking;

View File

@@ -1,12 +1,12 @@
use super::*;
#[cfg(test)]
use crate::generated_asset_sheets::crop_generated_asset_sheet_view_edge_matte;
use crate::generated_asset_sheets::{
GeneratedAssetSheetPersistInput, GeneratedAssetSheetPersistPrompt,
GeneratedAssetSheetPromptInput, GeneratedAssetSheetSliceImage,
build_generated_asset_sheet_prompt, persist_generated_asset_sheet_bytes,
slice_generated_asset_sheet,
};
#[cfg(test)]
use crate::generated_asset_sheets::crop_generated_asset_sheet_view_edge_matte;
pub(super) async fn generate_match3d_item_assets(
state: &AppState,

View File

@@ -14,3 +14,4 @@ pub mod profile;
pub mod puzzle;
pub mod square_hole;
pub mod story;
pub mod wooden_fish;

View File

@@ -0,0 +1,80 @@
use axum::{
Router, middleware,
routing::{get, post},
};
use crate::{
auth::require_bearer_auth,
state::AppState,
wooden_fish::{
checkpoint_wooden_fish_run, create_wooden_fish_session, execute_wooden_fish_action,
finish_wooden_fish_run, get_wooden_fish_gallery_detail, get_wooden_fish_runtime_work,
get_wooden_fish_session, list_wooden_fish_gallery, publish_wooden_fish_work,
start_wooden_fish_run,
},
};
pub fn router(state: AppState) -> Router<AppState> {
Router::new()
.route(
"/api/creation/wooden-fish/sessions",
post(create_wooden_fish_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/wooden-fish/sessions/{session_id}",
get(get_wooden_fish_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/wooden-fish/sessions/{session_id}/actions",
post(execute_wooden_fish_action).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/wooden-fish/works/{profile_id}/publish",
post(publish_wooden_fish_work).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/wooden-fish/works/{profile_id}",
get(get_wooden_fish_runtime_work),
)
.route(
"/api/runtime/wooden-fish/runs",
post(start_wooden_fish_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/wooden-fish/runs/{run_id}/checkpoint",
post(checkpoint_wooden_fish_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/wooden-fish/runs/{run_id}/finish",
post(finish_wooden_fish_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/wooden-fish/gallery",
get(list_wooden_fish_gallery),
)
.route(
"/api/runtime/wooden-fish/gallery/{public_work_code}",
get(get_wooden_fish_gallery_detail),
)
}

View File

@@ -1258,9 +1258,7 @@ fn build_admin_runtime(
#[cfg(debug_assertions)]
fn is_missing_creation_entry_config_procedure(error: &SpacetimeClientError) -> bool {
match error {
SpacetimeClientError::Procedure(message) => {
message.contains("No such procedure")
}
SpacetimeClientError::Procedure(message) => message.contains("No such procedure"),
_ => false,
}
}

View File

@@ -233,13 +233,15 @@ pub async fn create_visual_novel_sound_effect_task(
}
pub async fn create_sound_effect_task(
State(_state): State<AppState>,
State(state): State<AppState>,
axum::extract::Extension(request_context): axum::extract::Extension<RequestContext>,
payload: Result<Json<creation_audio::CreateSoundEffectRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let _ = parse_json_payload(&request_context, payload)?;
Err(creation_audio_generation_disabled_error()
.into_response_with_context(Some(&request_context)))
let Json(payload) = parse_json_payload(&request_context, payload)?;
create_sound_effect_task_response(&state, payload.prompt, payload.duration, payload.seed)
.await
.map(|task| json_success_body(Some(&request_context), task))
.map_err(|error| error.into_response_with_context(Some(&request_context)))
}
pub(crate) async fn generate_sound_effect_asset_for_creation(
@@ -518,15 +520,25 @@ pub async fn publish_background_music_asset(
}
pub async fn publish_sound_effect_asset(
State(_state): State<AppState>,
Path(_task_id): Path<String>,
State(state): State<AppState>,
Path(task_id): Path<String>,
axum::extract::Extension(request_context): axum::extract::Extension<RequestContext>,
axum::extract::Extension(_authenticated): axum::extract::Extension<AuthenticatedAccessToken>,
axum::extract::Extension(authenticated): axum::extract::Extension<AuthenticatedAccessToken>,
payload: Result<Json<creation_audio::PublishGeneratedAudioAssetRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let payload = parse_json_payload(&request_context, payload)?.0;
Err(creation_audio_generation_disabled_error_for_target(payload)
.into_response_with_context(Some(&request_context)))
let target = build_creation_audio_target(payload, AudioAssetSlot::SoundEffect)
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
publish_generated_audio_asset(
&state,
authenticated.claims().user_id(),
task_id,
AudioAssetSlot::SoundEffect,
target,
)
.await
.map(|payload| json_success_body(Some(&request_context), payload))
.map_err(|error| error.into_response_with_context(Some(&request_context)))
}
async fn publish_generated_audio_asset(
@@ -860,10 +872,36 @@ fn build_visual_novel_audio_target(
})
}
fn build_creation_audio_target(
payload: creation_audio::PublishGeneratedAudioAssetRequest,
slot: AudioAssetSlot,
) -> Result<AudioAssetBindingTarget, AppError> {
if matches!(slot, AudioAssetSlot::SoundEffect)
&& payload.entity_kind.trim() == "wooden_fish_work"
&& payload.slot.trim() == "hit_sound"
&& payload.asset_kind.trim() == "wooden_fish_hit_sound"
&& payload.storage_prefix
== Some(creation_audio::CreationAudioStoragePrefix::WoodenFishAssets)
{
let entity_id = normalize_limited_text(&payload.entity_id, "entityId", 160)?;
return Ok(AudioAssetBindingTarget {
storage_scope: payload.entity_kind.trim().to_string(),
entity_kind: payload.entity_kind.trim().to_string(),
entity_id,
slot: payload.slot.trim().to_string(),
asset_kind: payload.asset_kind.trim().to_string(),
profile_id: normalize_optional_text(payload.profile_id.as_deref()),
storage_prefix: LegacyAssetPrefix::WoodenFishAssets,
});
}
Err(creation_audio_generation_disabled_error_for_target(payload))
}
fn creation_audio_generation_disabled_error() -> AppError {
AppError::from_status(StatusCode::GONE).with_details(json!({
"provider": VECTOR_ENGINE_PROVIDER,
"message": "拼图与抓大鹅音频生成入口已临时关闭",
"message": "当前创作音频目标未开放",
}))
}
@@ -872,8 +910,9 @@ fn creation_audio_generation_disabled_error_for_target(
) -> AppError {
creation_audio_generation_disabled_error().with_details(json!({
"provider": VECTOR_ENGINE_PROVIDER,
"message": "拼图与抓大鹅音频生成入口已临时关闭",
"message": "当前创作音频目标未开放",
"entityKind": payload.entity_kind.trim(),
"slot": payload.slot.trim(),
}))
}
@@ -1434,7 +1473,7 @@ mod tests {
}
#[test]
fn disabled_creation_audio_targets_return_gone() {
fn disabled_creation_audio_targets_return_gone_except_wooden_fish_sound_effects() {
let payload = creation_audio::PublishGeneratedAudioAssetRequest {
entity_kind: "puzzle_work".to_string(),
entity_id: "puzzle-profile-1".to_string(),
@@ -1467,6 +1506,22 @@ mod tests {
};
let error = creation_audio_generation_disabled_error_for_target(payload);
assert_eq!(error.status_code(), StatusCode::GONE);
let payload = creation_audio::PublishGeneratedAudioAssetRequest {
entity_kind: "wooden_fish_work".to_string(),
entity_id: "wooden-fish-profile-1".to_string(),
slot: "hit_sound".to_string(),
asset_kind: "wooden_fish_hit_sound".to_string(),
profile_id: Some("wooden-fish-profile-1".to_string()),
storage_prefix: Some(creation_audio::CreationAudioStoragePrefix::WoodenFishAssets),
};
let target = build_creation_audio_target(payload, AudioAssetSlot::SoundEffect)
.expect("wooden fish hit sound target should be enabled");
assert_eq!(target.entity_kind, "wooden_fish_work");
assert_eq!(target.slot, "hit_sound");
assert_eq!(target.storage_prefix, LegacyAssetPrefix::WoodenFishAssets);
assert_eq!(target.storage_scope, "wooden_fish_work");
}
#[test]

File diff suppressed because it is too large Load Diff