Skip to content

Commit 21d01c6

Browse files
Fix persona type + aligned_lyrics shape from live API testing
Two bugs found by actually hitting the live Suno API: 1. Persona endpoint returns {"persona": {...}}, not {"items": [...]}. Fixed PersonaResponse to match real API shape. Now includes user_display_name, user_handle, and persona_clips from live data. 2. Aligned lyrics endpoint returns {"aligned_words": [...]}, not a bare array. Was causing timed-lyrics and lyrics embedding to fail silently. Now extracts the nested array correctly. Both were invisible to compile-time checks — only discoverable by testing against the live API.
1 parent f010007 commit 21d01c6

4 files changed

Lines changed: 22 additions & 25 deletions

File tree

src/api/metadata.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ impl SunoClient {
3939
.send()
4040
.await?;
4141
let resp = self.check_response(resp).await?;
42-
Ok(resp.json().await?)
42+
let body: serde_json::Value = resp.json().await?;
43+
// API returns {"aligned_words": [...], ...} — extract the array
44+
let words = body.get("aligned_words").ok_or_else(|| CliError::Api {
45+
code: "missing_field",
46+
message: "aligned_lyrics response missing 'aligned_words' field".into(),
47+
})?;
48+
Ok(serde_json::from_value(words.clone())?)
4349
}
4450

4551
/// Check whether captcha is required before generation.

src/api/persona.rs

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::SunoClient;
2-
use super::types::PersonaInfo;
2+
use super::types::{PersonaInfo, PersonaResponse};
33
use crate::errors::CliError;
44

55
impl SunoClient {
@@ -13,19 +13,7 @@ impl SunoClient {
1313
.send()
1414
.await?;
1515
let resp = self.check_response(resp).await?;
16-
17-
let body: serde_json::Value = resp.json().await?;
18-
19-
if let Some(first) = body
20-
.get("items")
21-
.and_then(|v| v.as_array())
22-
.and_then(|items| items.first())
23-
{
24-
let info: PersonaInfo = serde_json::from_value(first.clone())?;
25-
return Ok(info);
26-
}
27-
28-
let info: PersonaInfo = serde_json::from_value(body)?;
29-
Ok(info)
16+
let body: PersonaResponse = resp.json().await?;
17+
Ok(body.persona)
3018
}
3119
}

src/api/types.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,9 @@ pub struct ConcatRequest {
276276

277277
// --- Persona ---
278278

279-
#[allow(dead_code)]
280279
#[derive(Debug, Deserialize, Serialize)]
281280
pub struct PersonaResponse {
282-
#[serde(default)]
283-
pub items: Vec<PersonaInfo>,
281+
pub persona: PersonaInfo,
284282
}
285283

286284
#[derive(Debug, Deserialize, Serialize)]
@@ -290,9 +288,11 @@ pub struct PersonaInfo {
290288
#[serde(default)]
291289
pub description: Option<String>,
292290
#[serde(default)]
293-
pub image_url: Option<String>,
291+
pub image_s3_id: Option<String>,
292+
#[serde(default)]
293+
pub user_display_name: Option<String>,
294294
#[serde(default)]
295-
pub created_at: Option<String>,
295+
pub user_handle: Option<String>,
296296
#[serde(default)]
297-
pub num_clips: u64,
297+
pub persona_clips: Vec<serde_json::Value>,
298298
}

src/output/table.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,13 @@ pub fn persona(info: &PersonaInfo) {
161161
"Description",
162162
info.description.as_deref().unwrap_or("-"),
163163
]);
164-
table.add_row(vec!["Clips", &info.num_clips.to_string()]);
165-
if let Some(ref created) = info.created_at {
166-
table.add_row(vec!["Created", created]);
164+
if let Some(ref owner) = info.user_display_name {
165+
table.add_row(vec!["Owner", owner]);
167166
}
167+
if let Some(ref handle) = info.user_handle {
168+
table.add_row(vec!["Handle", handle]);
169+
}
170+
table.add_row(vec!["Clips", &info.persona_clips.len().to_string()]);
168171

169172
println!("{table}");
170173
}

0 commit comments

Comments
 (0)