Merge branch 'master' into codex/refine-creation-progress-wooden-fish
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::tracking::record_external_generation_run_after_success;
|
||||
|
||||
struct BigFishDashScopeSettings {
|
||||
base_url: String,
|
||||
@@ -39,52 +40,99 @@ pub(super) async fn generate_big_fish_formal_asset(
|
||||
motion_key: Option<&str>,
|
||||
generated_at_micros: i64,
|
||||
) -> Result<String, AppError> {
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
.get_big_fish_session(session_id.to_string(), owner_user_id.to_string())
|
||||
.await
|
||||
.map_err(map_big_fish_client_error)?;
|
||||
let draft = session.draft.as_ref().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "big-fish",
|
||||
"message": "玩法草稿尚未编译,不能生成正式图片。",
|
||||
}))
|
||||
})?;
|
||||
let context = build_big_fish_formal_asset_context(
|
||||
&session,
|
||||
draft,
|
||||
asset_kind,
|
||||
level,
|
||||
motion_key,
|
||||
generated_at_micros,
|
||||
)?;
|
||||
let settings = require_big_fish_dashscope_settings(state)?;
|
||||
let http_client = build_big_fish_dashscope_http_client(&settings)?;
|
||||
let generated = create_big_fish_text_to_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
context.prompt.as_str(),
|
||||
context.negative_prompt.as_str(),
|
||||
context.size.as_str(),
|
||||
)
|
||||
.await?;
|
||||
let downloaded = download_big_fish_remote_image(
|
||||
&http_client,
|
||||
generated.image_url.as_str(),
|
||||
"下载 Big Fish 正式图片失败",
|
||||
context.apply_transparent_background_post_process,
|
||||
)
|
||||
.await?;
|
||||
let started_at_micros = current_utc_micros();
|
||||
let request_payload = json!({
|
||||
"assetKind": asset_kind,
|
||||
"level": level,
|
||||
"motionKey": motion_key,
|
||||
"sessionId": session_id,
|
||||
"ownerUserId": owner_user_id,
|
||||
});
|
||||
let outcome = async {
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
.get_big_fish_session(session_id.to_string(), owner_user_id.to_string())
|
||||
.await
|
||||
.map_err(map_big_fish_client_error)?;
|
||||
let draft = session.draft.as_ref().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "big-fish",
|
||||
"message": "玩法草稿尚未编译,不能生成正式图片。",
|
||||
}))
|
||||
})?;
|
||||
let context = build_big_fish_formal_asset_context(
|
||||
&session,
|
||||
draft,
|
||||
asset_kind,
|
||||
level,
|
||||
motion_key,
|
||||
generated_at_micros,
|
||||
)?;
|
||||
let settings = require_big_fish_dashscope_settings(state)?;
|
||||
let http_client = build_big_fish_dashscope_http_client(&settings)?;
|
||||
let generated = create_big_fish_text_to_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
context.prompt.as_str(),
|
||||
context.negative_prompt.as_str(),
|
||||
context.size.as_str(),
|
||||
)
|
||||
.await?;
|
||||
let downloaded = download_big_fish_remote_image(
|
||||
&http_client,
|
||||
generated.image_url.as_str(),
|
||||
"下载 Big Fish 正式图片失败",
|
||||
context.apply_transparent_background_post_process,
|
||||
)
|
||||
.await?;
|
||||
|
||||
persist_big_fish_formal_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
&context,
|
||||
generated,
|
||||
downloaded,
|
||||
generated_at_micros,
|
||||
)
|
||||
.await
|
||||
persist_big_fish_formal_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
&context,
|
||||
generated,
|
||||
downloaded,
|
||||
generated_at_micros,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.await;
|
||||
match outcome {
|
||||
Ok(value) => {
|
||||
record_external_generation_run_after_success(
|
||||
state,
|
||||
"dashscope",
|
||||
"big_fish_text_to_image",
|
||||
"大鱼正式图片生成",
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
Some(json!({
|
||||
"legacyPublicPath": value.clone(),
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
Ok(value)
|
||||
}
|
||||
Err(error) => {
|
||||
record_external_generation_run_after_success(
|
||||
state,
|
||||
"dashscope",
|
||||
"big_fish_text_to_image",
|
||||
"大鱼正式图片生成",
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
false,
|
||||
Some(error.to_string()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_big_fish_formal_asset_context(
|
||||
@@ -626,6 +674,10 @@ fn map_big_fish_asset_binding_prepare_error(error: AssetObjectFieldError) -> App
|
||||
}))
|
||||
}
|
||||
|
||||
fn current_utc_micros() -> i64 {
|
||||
(time::OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000) as i64
|
||||
}
|
||||
|
||||
fn map_big_fish_asset_spacetime_error(error: SpacetimeClientError) -> AppError {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "spacetimedb",
|
||||
|
||||
@@ -8,6 +8,7 @@ use platform_image::{
|
||||
vector_engine_images_generation_url,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
external_api_audit::{
|
||||
@@ -16,6 +17,7 @@ use crate::{
|
||||
},
|
||||
http_error::AppError,
|
||||
state::AppState,
|
||||
tracking::record_external_generation_run_after_success,
|
||||
};
|
||||
|
||||
pub(crate) use platform_image::GPT_IMAGE_2_MODEL;
|
||||
@@ -105,6 +107,14 @@ pub(crate) async fn create_openai_image_generation(
|
||||
reference_images: &[String],
|
||||
failure_context: &str,
|
||||
) -> Result<OpenAiGeneratedImages, AppError> {
|
||||
let started_at_micros = current_utc_micros();
|
||||
let request_payload = json!({
|
||||
"size": size,
|
||||
"candidateCount": candidate_count,
|
||||
"promptChars": prompt.chars().count(),
|
||||
"negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count),
|
||||
"referenceImageCount": reference_images.len(),
|
||||
});
|
||||
let result = create_vector_engine_image_generation(
|
||||
http_client,
|
||||
&settings.provider_settings(),
|
||||
@@ -116,7 +126,15 @@ pub(crate) async fn create_openai_image_generation(
|
||||
failure_context,
|
||||
)
|
||||
.await;
|
||||
map_platform_image_result(settings, result).await
|
||||
map_platform_image_result(
|
||||
settings,
|
||||
result,
|
||||
"image_generation",
|
||||
failure_context,
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_openai_image_edit(
|
||||
@@ -128,6 +146,13 @@ pub(crate) async fn create_openai_image_edit(
|
||||
reference_image: &OpenAiReferenceImage,
|
||||
failure_context: &str,
|
||||
) -> Result<OpenAiGeneratedImages, AppError> {
|
||||
let started_at_micros = current_utc_micros();
|
||||
let request_payload = json!({
|
||||
"size": size,
|
||||
"promptChars": prompt.chars().count(),
|
||||
"negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count),
|
||||
"referenceImageCount": 1,
|
||||
});
|
||||
let result = create_vector_engine_image_edit(
|
||||
http_client,
|
||||
&settings.provider_settings(),
|
||||
@@ -138,7 +163,15 @@ pub(crate) async fn create_openai_image_edit(
|
||||
failure_context,
|
||||
)
|
||||
.await;
|
||||
map_platform_image_result(settings, result).await
|
||||
map_platform_image_result(
|
||||
settings,
|
||||
result,
|
||||
"image_edit",
|
||||
failure_context,
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_openai_image_edit_with_references(
|
||||
@@ -151,6 +184,14 @@ pub(crate) async fn create_openai_image_edit_with_references(
|
||||
reference_images: &[OpenAiReferenceImage],
|
||||
failure_context: &str,
|
||||
) -> Result<OpenAiGeneratedImages, AppError> {
|
||||
let started_at_micros = current_utc_micros();
|
||||
let request_payload = json!({
|
||||
"size": size,
|
||||
"candidateCount": candidate_count,
|
||||
"promptChars": prompt.chars().count(),
|
||||
"negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count),
|
||||
"referenceImageCount": reference_images.len(),
|
||||
});
|
||||
let result = create_vector_engine_image_edit_with_references(
|
||||
http_client,
|
||||
&settings.provider_settings(),
|
||||
@@ -162,7 +203,15 @@ pub(crate) async fn create_openai_image_edit_with_references(
|
||||
failure_context,
|
||||
)
|
||||
.await;
|
||||
map_platform_image_result(settings, result).await
|
||||
map_platform_image_result(
|
||||
settings,
|
||||
result,
|
||||
"image_edit_with_references",
|
||||
failure_context,
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn download_remote_image(
|
||||
@@ -200,19 +249,57 @@ impl OpenAiImageSettings {
|
||||
}
|
||||
}
|
||||
|
||||
async fn map_platform_image_result<T>(
|
||||
async fn map_platform_image_result(
|
||||
settings: &OpenAiImageSettings,
|
||||
result: Result<T, PlatformImageError>,
|
||||
) -> Result<T, AppError> {
|
||||
result: Result<OpenAiGeneratedImages, PlatformImageError>,
|
||||
operation: &'static str,
|
||||
failure_context: &str,
|
||||
request_payload: Value,
|
||||
started_at_micros: i64,
|
||||
) -> Result<OpenAiGeneratedImages, AppError> {
|
||||
match result {
|
||||
Ok(value) => Ok(value),
|
||||
Ok(value) => {
|
||||
if let Some(state) = settings.external_api_audit_state.as_ref() {
|
||||
record_external_generation_run_after_success(
|
||||
state,
|
||||
VECTOR_ENGINE_PROVIDER,
|
||||
operation,
|
||||
failure_context,
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
true,
|
||||
None,
|
||||
Some(value.task_id.clone()),
|
||||
Some(json!({
|
||||
"imageCount": value.images.len(),
|
||||
"actualPromptChars": value.actual_prompt.as_ref().map(|prompt| prompt.chars().count()),
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(state) = settings.external_api_audit_state.as_ref() {
|
||||
record_external_generation_run_after_success(
|
||||
state,
|
||||
VECTOR_ENGINE_PROVIDER,
|
||||
operation,
|
||||
failure_context,
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
false,
|
||||
Some(error.message().to_string()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
record_openai_image_failure_if_configured(settings, &error).await;
|
||||
Err(map_platform_image_error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn record_openai_image_failure_if_configured(
|
||||
settings: &OpenAiImageSettings,
|
||||
error: &PlatformImageError,
|
||||
@@ -457,3 +544,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn current_utc_micros() -> i64 {
|
||||
(OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000) as i64
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ use spacetime_client::{
|
||||
PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord,
|
||||
PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord,
|
||||
PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunPauseRecordInput,
|
||||
PuzzleRunPropRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput,
|
||||
PuzzleRunSwapRecordInput, PuzzleSelectCoverImageRecordInput, PuzzleUiBackgroundSaveRecordInput,
|
||||
PuzzleRunPropRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
|
||||
PuzzleSelectCoverImageRecordInput, PuzzleUiBackgroundSaveRecordInput,
|
||||
PuzzleWorkLikeReportRecordInput, PuzzleWorkPointIncentiveClaimRecordInput,
|
||||
PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput,
|
||||
SpacetimeClientError,
|
||||
|
||||
@@ -53,6 +53,55 @@ struct RouteTrackingSpec {
|
||||
scope_id: &'static str,
|
||||
}
|
||||
|
||||
pub async fn record_external_generation_run_after_success(
|
||||
state: &AppState,
|
||||
provider: &str,
|
||||
operation: &str,
|
||||
request_label: &str,
|
||||
request_payload: Value,
|
||||
started_at_micros: i64,
|
||||
success: bool,
|
||||
failure_reason: Option<String>,
|
||||
provider_request_id: Option<String>,
|
||||
result_payload: Option<Value>,
|
||||
) {
|
||||
let completed_at_micros = current_utc_micros();
|
||||
let duration_ms = completed_at_micros.saturating_sub(started_at_micros).max(0) / 1_000;
|
||||
let mut draft = TrackingEventDraft::new("external_generation_run", "external-generation");
|
||||
draft.scope_kind = RuntimeTrackingScopeKind::Module;
|
||||
draft.scope_id = provider.to_string();
|
||||
draft.metadata = json!({
|
||||
"runId": format!("external-generation-{}", Uuid::new_v4()),
|
||||
"provider": provider,
|
||||
"operation": operation,
|
||||
"requestLabel": request_label.trim(),
|
||||
"requestPayload": request_payload,
|
||||
"status": if success { "succeeded" } else { "failed" },
|
||||
"success": success,
|
||||
"failureReason": failure_reason,
|
||||
"providerRequestId": provider_request_id,
|
||||
"resultPayload": result_payload,
|
||||
"startedAtMicros": started_at_micros,
|
||||
"completedAtMicros": completed_at_micros,
|
||||
"durationMs": duration_ms,
|
||||
});
|
||||
|
||||
record_tracking_event_after_success(state, &external_generation_request_context(), draft).await;
|
||||
}
|
||||
|
||||
fn current_utc_micros() -> i64 {
|
||||
(OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000) as i64
|
||||
}
|
||||
|
||||
fn external_generation_request_context() -> RequestContext {
|
||||
RequestContext::new(
|
||||
format!("external-generation-{}", Uuid::new_v4()),
|
||||
"external generation run".to_string(),
|
||||
std::time::Duration::ZERO,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn record_route_tracking_event_after_success(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use serde_json::json;
|
||||
use shared_contracts::creation_audio;
|
||||
|
||||
use crate::{http_error::AppError, state::AppState};
|
||||
use crate::{
|
||||
http_error::AppError, state::AppState, tracking::record_external_generation_run_after_success,
|
||||
};
|
||||
|
||||
use super::{
|
||||
clock::current_utc_iso_text,
|
||||
clock::{current_utc_iso_text, current_utc_micros},
|
||||
errors::{map_platform_audio_error, vector_engine_bad_gateway},
|
||||
publish::wait_for_generated_audio_asset,
|
||||
tasks::{create_background_music_task_response, create_sound_effect_task_response},
|
||||
@@ -18,45 +21,69 @@ pub(crate) async fn generate_sound_effect_asset_for_creation(
|
||||
seed: Option<u64>,
|
||||
target: GeneratedCreationAudioTarget,
|
||||
) -> Result<creation_audio::CreationAudioAsset, AppError> {
|
||||
let started_at_micros = current_utc_micros();
|
||||
let normalized_prompt = platform_audio::normalize_limited_text(
|
||||
&prompt,
|
||||
"prompt",
|
||||
platform_audio::VIDU_PROMPT_MAX_CHARS,
|
||||
)
|
||||
.map_err(map_platform_audio_error)?;
|
||||
let task =
|
||||
create_sound_effect_task_response(state, normalized_prompt.clone(), duration, seed).await?;
|
||||
let target = AudioAssetBindingTarget {
|
||||
storage_scope: target.entity_kind.clone(),
|
||||
entity_kind: target.entity_kind,
|
||||
entity_id: target.entity_id,
|
||||
slot: target.slot,
|
||||
asset_kind: target.asset_kind,
|
||||
profile_id: target.profile_id,
|
||||
storage_prefix: target.storage_prefix,
|
||||
};
|
||||
let generated = wait_for_generated_audio_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
task.task_id.clone(),
|
||||
AudioAssetSlot::SoundEffect,
|
||||
target,
|
||||
)
|
||||
.await?;
|
||||
let audio_src = generated
|
||||
.audio_src
|
||||
.ok_or_else(|| vector_engine_bad_gateway("音效生成完成但缺少播放地址"))?;
|
||||
let request_payload = json!({
|
||||
"kind": "sound_effect",
|
||||
"promptChars": normalized_prompt.chars().count(),
|
||||
"duration": duration,
|
||||
"seed": seed,
|
||||
"targetEntityKind": target.entity_kind,
|
||||
"targetEntityId": target.entity_id,
|
||||
"targetSlot": target.slot,
|
||||
"targetAssetKind": target.asset_kind,
|
||||
});
|
||||
let outcome = async {
|
||||
let task =
|
||||
create_sound_effect_task_response(state, normalized_prompt.clone(), duration, seed)
|
||||
.await?;
|
||||
let target = AudioAssetBindingTarget {
|
||||
storage_scope: target.entity_kind.clone(),
|
||||
entity_kind: target.entity_kind,
|
||||
entity_id: target.entity_id,
|
||||
slot: target.slot,
|
||||
asset_kind: target.asset_kind,
|
||||
profile_id: target.profile_id,
|
||||
storage_prefix: target.storage_prefix,
|
||||
};
|
||||
let generated = wait_for_generated_audio_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
task.task_id.clone(),
|
||||
AudioAssetSlot::SoundEffect,
|
||||
target,
|
||||
)
|
||||
.await?;
|
||||
let audio_src = generated
|
||||
.audio_src
|
||||
.ok_or_else(|| vector_engine_bad_gateway("音效生成完成但缺少播放地址"))?;
|
||||
|
||||
Ok(creation_audio::CreationAudioAsset {
|
||||
task_id: generated.task_id,
|
||||
provider: generated.provider,
|
||||
asset_object_id: generated.asset_object_id,
|
||||
asset_kind: generated.asset_kind,
|
||||
audio_src,
|
||||
prompt: Some(normalized_prompt),
|
||||
title: None,
|
||||
updated_at: Some(current_utc_iso_text()),
|
||||
})
|
||||
Ok::<_, AppError>(creation_audio::CreationAudioAsset {
|
||||
task_id: generated.task_id,
|
||||
provider: generated.provider,
|
||||
asset_object_id: generated.asset_object_id,
|
||||
asset_kind: generated.asset_kind,
|
||||
audio_src,
|
||||
prompt: Some(normalized_prompt),
|
||||
title: None,
|
||||
updated_at: Some(current_utc_iso_text()),
|
||||
})
|
||||
}
|
||||
.await;
|
||||
record_creation_audio_generation_run(
|
||||
state,
|
||||
"sound_effect",
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
&outcome,
|
||||
)
|
||||
.await;
|
||||
outcome
|
||||
}
|
||||
|
||||
pub(crate) async fn generate_background_music_asset_for_creation(
|
||||
@@ -68,6 +95,7 @@ pub(crate) async fn generate_background_music_asset_for_creation(
|
||||
model: Option<String>,
|
||||
target: GeneratedCreationAudioTarget,
|
||||
) -> Result<creation_audio::CreationAudioAsset, AppError> {
|
||||
let started_at_micros = current_utc_micros();
|
||||
let normalized_prompt = platform_audio::normalize_limited_text_allow_empty(
|
||||
&prompt,
|
||||
"prompt",
|
||||
@@ -80,43 +108,111 @@ pub(crate) async fn generate_background_music_asset_for_creation(
|
||||
platform_audio::SUNO_TITLE_MAX_CHARS,
|
||||
)
|
||||
.map_err(map_platform_audio_error)?;
|
||||
let task = create_background_music_task_response(
|
||||
state,
|
||||
normalized_prompt.clone(),
|
||||
normalized_title.clone(),
|
||||
tags,
|
||||
model,
|
||||
)
|
||||
.await?;
|
||||
let target = AudioAssetBindingTarget {
|
||||
storage_scope: target.entity_kind.clone(),
|
||||
entity_kind: target.entity_kind,
|
||||
entity_id: target.entity_id,
|
||||
slot: target.slot,
|
||||
asset_kind: target.asset_kind,
|
||||
profile_id: target.profile_id,
|
||||
storage_prefix: target.storage_prefix,
|
||||
};
|
||||
let generated = wait_for_generated_audio_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
task.task_id.clone(),
|
||||
AudioAssetSlot::BackgroundMusic,
|
||||
target,
|
||||
)
|
||||
.await?;
|
||||
let audio_src = generated
|
||||
.audio_src
|
||||
.ok_or_else(|| vector_engine_bad_gateway("背景音乐生成完成但缺少播放地址"))?;
|
||||
let request_payload = json!({
|
||||
"kind": "background_music",
|
||||
"promptChars": normalized_prompt.chars().count(),
|
||||
"titleChars": normalized_title.chars().count(),
|
||||
"hasTags": tags.as_ref().is_some_and(|value| !value.trim().is_empty()),
|
||||
"model": model,
|
||||
"targetEntityKind": target.entity_kind,
|
||||
"targetEntityId": target.entity_id,
|
||||
"targetSlot": target.slot,
|
||||
"targetAssetKind": target.asset_kind,
|
||||
});
|
||||
let outcome = async {
|
||||
let task = create_background_music_task_response(
|
||||
state,
|
||||
normalized_prompt.clone(),
|
||||
normalized_title.clone(),
|
||||
tags,
|
||||
model,
|
||||
)
|
||||
.await?;
|
||||
let target = AudioAssetBindingTarget {
|
||||
storage_scope: target.entity_kind.clone(),
|
||||
entity_kind: target.entity_kind,
|
||||
entity_id: target.entity_id,
|
||||
slot: target.slot,
|
||||
asset_kind: target.asset_kind,
|
||||
profile_id: target.profile_id,
|
||||
storage_prefix: target.storage_prefix,
|
||||
};
|
||||
let generated = wait_for_generated_audio_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
task.task_id.clone(),
|
||||
AudioAssetSlot::BackgroundMusic,
|
||||
target,
|
||||
)
|
||||
.await?;
|
||||
let audio_src = generated
|
||||
.audio_src
|
||||
.ok_or_else(|| vector_engine_bad_gateway("背景音乐生成完成但缺少播放地址"))?;
|
||||
|
||||
Ok(creation_audio::CreationAudioAsset {
|
||||
task_id: generated.task_id,
|
||||
provider: generated.provider,
|
||||
asset_object_id: generated.asset_object_id,
|
||||
asset_kind: generated.asset_kind,
|
||||
audio_src,
|
||||
prompt: Some(normalized_prompt),
|
||||
title: Some(normalized_title),
|
||||
updated_at: Some(current_utc_iso_text()),
|
||||
})
|
||||
Ok::<_, AppError>(creation_audio::CreationAudioAsset {
|
||||
task_id: generated.task_id,
|
||||
provider: generated.provider,
|
||||
asset_object_id: generated.asset_object_id,
|
||||
asset_kind: generated.asset_kind,
|
||||
audio_src,
|
||||
prompt: Some(normalized_prompt),
|
||||
title: Some(normalized_title),
|
||||
updated_at: Some(current_utc_iso_text()),
|
||||
})
|
||||
}
|
||||
.await;
|
||||
record_creation_audio_generation_run(
|
||||
state,
|
||||
"background_music",
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
&outcome,
|
||||
)
|
||||
.await;
|
||||
outcome
|
||||
}
|
||||
|
||||
async fn record_creation_audio_generation_run(
|
||||
state: &AppState,
|
||||
operation: &'static str,
|
||||
request_payload: serde_json::Value,
|
||||
started_at_micros: i64,
|
||||
outcome: &Result<creation_audio::CreationAudioAsset, AppError>,
|
||||
) {
|
||||
match outcome {
|
||||
Ok(asset) => {
|
||||
record_external_generation_run_after_success(
|
||||
state,
|
||||
asset.provider.as_str(),
|
||||
operation,
|
||||
"创作音频生成",
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
true,
|
||||
None,
|
||||
Some(asset.task_id.clone()),
|
||||
Some(json!({
|
||||
"assetObjectId": asset.asset_object_id,
|
||||
"assetKind": asset.asset_kind,
|
||||
"hasAudioSrc": !asset.audio_src.trim().is_empty(),
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(error) => {
|
||||
record_external_generation_run_after_success(
|
||||
state,
|
||||
"vector-engine-audio",
|
||||
operation,
|
||||
"创作音频生成",
|
||||
request_payload,
|
||||
started_at_micros,
|
||||
false,
|
||||
Some(error.to_string()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ use shared_contracts::wooden_fish::{
|
||||
WoodenFishDraftResponse, WoodenFishFinishRunRequest, WoodenFishGalleryDetailResponse,
|
||||
WoodenFishGenerationStatus, WoodenFishImageAsset, WoodenFishRunResponse,
|
||||
WoodenFishSessionResponse, WoodenFishSessionSnapshotResponse, WoodenFishStartRunRequest,
|
||||
WoodenFishWorkDetailResponse, WoodenFishWorkMutationResponse, WoodenFishWorkspaceCreateRequest,
|
||||
WoodenFishWorksResponse,
|
||||
WoodenFishWorkDetailResponse, WoodenFishWorkMutationResponse, WoodenFishWorksResponse,
|
||||
WoodenFishWorkspaceCreateRequest,
|
||||
};
|
||||
use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros};
|
||||
use spacetime_client::SpacetimeClientError;
|
||||
|
||||
Reference in New Issue
Block a user