Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/api/cover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ impl SunoClient {
clip_id: &str,
model_key: &str,
tags: Option<&str>,
title: Option<&str>,
) -> Result<Vec<Clip>, CliError> {
let mut req = GenerateRequest::new(model_key, "cover");
req.tags = tags.map(String::from);
req.cover_clip_id = Some(clip_id.to_string());
// v2-web rejects a null `params.title` with HTTP 422, so it must always
// be a string. Use the caller's title, else inherit the source clip's
// title (matching the web app), else a generic fallback.
req.title = Some(self.resolve_title(clip_id, title, "Cover").await);
self.generate(&req).await
}
}
27 changes: 27 additions & 0 deletions src/api/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,33 @@ impl SunoClient {
.await
}

/// Resolve the title for a clip-derived generation (cover/remaster/extend).
/// Prefers an explicit override, then the source clip's own title, then a
/// generic fallback. `/api/generate/v2-web/` requires `params.title` to be a
/// non-null string, so this never returns an empty value.
pub async fn resolve_title(
&self,
clip_id: &str,
override_title: Option<&str>,
fallback: &str,
) -> String {
if let Some(t) = override_title {
let t = t.trim();
if !t.is_empty() {
return t.to_string();
}
}
if let Ok(clips) = self.get_clips(&[clip_id.to_string()]).await
&& let Some(c) = clips.into_iter().next()
{
let t = c.title.trim();
if !t.is_empty() {
return t.to_string();
}
}
fallback.to_string()
}

/// Poll clip status by IDs until all are complete or errored.
/// "streaming" means still generating — we wait for "complete".
pub async fn poll_clips(
Expand Down
4 changes: 4 additions & 0 deletions src/api/remaster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ impl SunoClient {
&self,
clip_id: &str,
remaster_model_key: &str,
title: Option<&str>,
) -> Result<Vec<Clip>, CliError> {
let mut req = GenerateRequest::new(remaster_model_key, "remaster");
req.cover_clip_id = Some(clip_id.to_string());
// v2-web rejects a null `params.title` with HTTP 422; always send a
// string (caller override, else the source clip's title, else fallback).
req.title = Some(self.resolve_title(clip_id, title, "Remaster").await);
self.generate(&req).await
}
}
12 changes: 12 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ pub struct ExtendArgs {
#[arg(long)]
pub tags: Option<String>,

/// Title for the new clip (defaults to the source clip's title)
#[arg(long)]
pub title: Option<String>,

/// Wait for completion
#[arg(short, long)]
pub wait: bool,
Expand All @@ -267,6 +271,10 @@ pub struct CoverArgs {
#[arg(long)]
pub tags: Option<String>,

/// Title for the cover (defaults to the source clip's title)
#[arg(long)]
pub title: Option<String>,

/// Model version for the cover
#[arg(short, long, default_value = "v5.5")]
pub model: ModelVersion,
Expand All @@ -289,6 +297,10 @@ pub struct RemasterArgs {
#[arg(long, default_value = "v5.5")]
pub model: RemasterModel,

/// Title for the remaster (defaults to the source clip's title)
#[arg(long)]
pub title: Option<String>,

/// Wait for completion
#[arg(short, long)]
pub wait: bool,
Expand Down
18 changes: 15 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,13 +375,18 @@ async fn run() -> Result<(), CliError> {
}

Commands::Extend(args) => {
let c = client().await?;
let mut req = GenerateRequest::new("chirp-fenix", "custom");
req.prompt = args.lyrics.unwrap_or_default();
req.tags = args.tags;
// v2-web requires a non-null `params.title`.
req.title = Some(
c.resolve_title(&args.clip_id, args.title.as_deref(), "Extend")
.await,
);
req.continue_clip_id = Some(args.clip_id);
req.continue_at = Some(args.at);

let c = client().await?;
let clips = c.generate(&req).await?;
handle_generation(&c, clips, args.wait, None, &fmt, cli.quiet).await?;
}
Expand All @@ -400,7 +405,12 @@ async fn run() -> Result<(), CliError> {
}
let c = client().await?;
let clips = c
.cover(&args.clip_id, args.model.to_api_key(), args.tags.as_deref())
.cover(
&args.clip_id,
args.model.to_api_key(),
args.tags.as_deref(),
args.title.as_deref(),
)
.await?;
handle_generation(
&c,
Expand All @@ -418,7 +428,9 @@ async fn run() -> Result<(), CliError> {
eprintln!("Remastering with {}...", args.model.to_api_key());
}
let c = client().await?;
let clips = c.remaster(&args.clip_id, args.model.to_api_key()).await?;
let clips = c
.remaster(&args.clip_id, args.model.to_api_key(), args.title.as_deref())
.await?;
handle_generation(
&c,
clips,
Expand Down