Skip to content

Music Edits ​

Music Edits changes a song you already have, in plain language, without regenerating the whole thing, and scores a video or an image with new music.

  • Section edit: pick a range of a song and say what should change ("make the chorus more energetic", "drop the drums for the first eight bars"). The range is regenerated from the surrounding audio and spliced back.
  • Lyric edit: re-sing one line with new words. Melody and voice are kept.
  • Soundtrack: music (and optionally sound effects, ducked under it) for a video or an image, in a mood you choose or one FOTOhub AI infers from the picture.

In every case everything outside the edited region stays sample-identical to your original, the seam is cross-faded and loudness-matched, and the result is a new version: you can chain edits and keep the earlier ones.

Availability

Music Edits is rolled out in stages. Until it is enabled for your account, every route answers 403 with {"error": "feature_disabled", "feature": "music_edit"}. Nothing is charged.

Endpoints ​

MethodPathChargedDescription
GET/v1/aiwave/music-edit/confignoPrices per operation, limits, stages, AI disclosure, where each step runs
POST/v1/aiwave/music-edit/section/quotenoPrice, availability, ETA and the normalised plan of a section edit
POST/v1/aiwave/music-edit/lyrics/quotenoSame, for a lyric line
POST/v1/aiwave/music-edit/soundtrack/quotenoSame, for a soundtrack
POST/v1/aiwave/music-edit/sectionyesRegenerate a section
POST/v1/aiwave/music-edit/lyricsyesRe-sing one line
POST/v1/aiwave/music-edit/soundtrackyesMusic for a video or an image
GET/v1/aiwave/music-edit/jobsnoYour history, newest first (?op=section|lyrics|soundtrack, ?limit=)
GET/v1/aiwave/music-edit/jobs/{job_id}noStatus, stage, result or error, refund state
POST/v1/aiwave/music-edit/jobs/{job_id}/cancelnoCancel; refunded once

Base URL: https://apis.fotohub.appAuthentication: Authorization: Bearer fh_live_your_api_key (see Authentication)

Pricing ​

Nothing is hard-coded here. A quote returns the price for your account before you commit, and the price keys are public:

  • GET /v1/aiwave/music-edit/config → prices maps each operation to its price key: section and lyric edits use aiw-music-edit, soundtracks use aiw-soundtrack.
  • Each quote returns credits, usd, available, eta_s, queue_depth and payment.
  • USD wallet rates are listed by GET /v1/pricing: GET /v1/pricing?model=aiw-music-edit. While the feature is disabled, GET /v1/pricing?model=aiw-* answers 404 for its keys.

Section edit ​

python
from fotohub import FotoHub

client = FotoHub()  # reads FOTOHUB_API_KEY

args = dict(
    source_duration_s=184.0, start_s=62.0, end_s=84.0,
    instruction="Make the chorus more energetic, brighter drums",
    lyrics="We run into the light",     # send the words for a sung section
)
quote = client.music_edit.quote_section("audio/<your-user-id>/song.flac", **args)
print(quote["usd"], quote["eta_s"], quote["available"])

job = client.music_edit.edit_section("audio/<your-user-id>/song.flac", **args)["job"]
done = client.music_edit.wait_for_job(job["job_id"], on_progress=lambda v: print(v["stage"]))
print(done["result"]["url"], done["result"]["verified_identical"])

# chain a second edit onto the first result
client.music_edit.edit_section(
    done["result"]["storage_path"], source_duration_s=184.0, start_s=10.0, end_s=20.0,
    instruction="Softer intro", parent_job_id=job["job_id"],
)
typescript
import { FotoHub } from "fotohub";

const client = new FotoHub({ apiKey: process.env.FOTOHUB_API_KEY! });

const { job } = (await client.musicEdit.editSection({
  sourcePath: "audio/<your-user-id>/song.flac",
  sourceDurationS: 184, startS: 62, endS: 84,
  instruction: "Make the chorus more energetic, brighter drums",
  lyrics: "We run into the light",
})) as any;
const done = await client.musicEdit.waitForJob(job.job_id);
console.log((done.result as any).url);
bash
curl https://apis.fotohub.app/v1/aiwave/music-edit/section \
  -H "Authorization: Bearer $FOTOHUB_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "source_path": "audio/<your-user-id>/song.flac",
    "source_duration_s": 184.0, "start_s": 62.0, "end_s": 84.0,
    "instruction": "Make the chorus more energetic, brighter drums",
    "lyrics": "We run into the light",
    "request_id": "6f1d3c52-8a0e-4b49-9c1f-0a2b3c4d5e6f"
  }'
FieldTypeDescription
source_pathstringRequired. A song in your own storage: audio/<your user id>/..., photos/<id>/... or videos/<id>/... (this is also where the result of an earlier edit lives)
source_duration_snumberRequired. Checked against the file; a mismatch beyond a small tolerance is refused
start_s, end_snumberRequired. The region, between limits.min_section_s and limits.max_section_s long
instructionstringRequired, up to limits.max_instruction_chars characters, Polish or English
lyricsstringThe words sung in the section. Send them for a sung section and the vocal is re-sung with them; without them the new section is instrumental
stylestringThe song's style, for example its original generation prompt
snap_to_beatboolDefault true: the region edges snap to the beat grid; the answer reports what was snapped
crossfade_msintegerSeam cross-fade, within limits.crossfade_ms.min to .max (default from the config)
seedinteger
parent_job_idUUIDThe job this edit builds on
request_idUUIDIdempotency key: the same request_id returns the job it already created (replayed: true) and never bills twice. The SDKs generate one

Lyric edit ​

POST /lyrics takes source_path, source_duration_s, line ({"start_s", "end_s", "text"}; text is the old words and optional), new_text and optionally style, crossfade_ms, seed, parent_job_id, request_id. The line must be between limits.min_line_s and limits.max_line_s long. Requests to imitate a specific person's voice are refused (422 voice_imitation).

python
client.music_edit.edit_lyrics(
    "audio/<your-user-id>/song.flac", source_duration_s=184.0,
    line={"start_s": 41.2, "end_s": 45.0, "text": "walking down the road"},
    new_text="running down the road",
)

Soundtrack from a video or an image ​

POST /soundtrack takes source_path (videos/<id>/... or photos/<id>/...), media_duration_s (required for a video), and optionally start_s (default 0), duration_s (between limits.soundtrack_s.min and .max, default from the config), mood, sfx (video only: add sound effects), keep_original_audio (video only), duck_db (how far the music dips under sound effects or the original audio, 0-18), seed, request_id.

FOTOhub AI looks at a few still frames of the video or at the image, describes the scene and writes music for it. Frames are not kept. Without a mood, the description chooses it; the result's brief says what was chosen (mood, genre, tempo_bpm, instruments, energy, scene).

python
job = client.music_edit.soundtrack(
    "videos/<your-user-id>/trailer.mp4", media_duration_s=42.0, duration_s=40, sfx=True
)["job"]
done = client.music_edit.wait_for_job(job["job_id"])
print(done["result"]["url"], done["result"]["preview_url"], done["result"]["brief"])

Jobs, results and refunds ​

Every charged call returns {"job": {...}, "replayed", "eta", "quote", "payment"}. Poll GET /jobs/{job_id}:

  • status: queued, running, succeeded, failed, cancelled; stage: queued, prepare, frames, caption, engine_queue, waiting_for_engine, generate, splice, mix; progress from 0 to 1; eta while it waits.
  • Section and lyric edits are queued: they run one at a time on the music engine, so eta and queue_depth can be non-zero.
  • Section / lyric result: url (FLAC), storage_path, format, duration_s, region (requested, snapped), beat, crossfade_ms, loudness, verified_identical (outside the region the samples equal your source), audition (before_url / after_url clips around the region, for A/B listening), peaks, seed, disclosure.
  • Soundtrack result: url (WAV), preview_url, storage_path, duration_s, layers, ducking, brief, stems (music, sfx, original), sfx_status, disclosure.
  • A failed or cancelled job has error: {"code", "message"} and is refunded exactly once (refunded, refund_message). POST .../cancel refunds a queued or running job.
  • Apply an edit as a new version of the song in your own tool; to edit again, use the result's storage_path as the next source_path and pass parent_job_id.

Labelling and where processing happens ​

  • Disclosure. Everything produced here is AI generated, at least in part. Every result carries disclosure (label: "Edited with FOTOhub AI", and a note). Keep the label where you publish the audio.
  • Where it runs. config().residency is authoritative. At the time of writing, music and sound-effect generation run on FOTOhub AI servers in the EU (France) and audio analysis, splicing and mixing in the EU (Poland). For a soundtrack, up to six still frames are described by FOTOhub AI vision to choose the music; the frames are not kept. Because that description step is a vision model, state in your own privacy copy that image analysis may run outside the EU rather than claiming EU-only processing.

Errors ​

StatuserrorMeaning
401authentication_requiredNo or invalid key
402insufficient_fundsWallet too low; required_usd, balance_usd, shortfall_usd, topup_url
403feature_disabledNot enabled for your account yet
404job_not_foundNot yours or gone
409not_cancellableThe job has already finished
422invalid_source_path, unsupported_file_name, invalid_region, region_too_short, region_too_long, source_too_long, voice_imitation, empty_lyrics, soundtrack_needs_video_or_image, sfx_needs_video, media_duration_required, invalid_durationFixable input; nothing charged
429ceiling_exceededkind: user_concurrency, daily_credits, rate; Retry-After; nothing charged
503backend_unavailableThe engine is busy or down; Retry-After; not charged

Quotes also report available: false with a reason (backend_unavailable, engine_saturated, capability_unavailable, cpu_tier_unavailable, pricing_not_configured): disable your primary button and tell the user nothing was charged.