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
| Method | Path | Charged | Description |
|---|---|---|---|
| GET | /v1/aiwave/music-edit/config | no | Prices per operation, limits, stages, AI disclosure, where each step runs |
| POST | /v1/aiwave/music-edit/section/quote | no | Price, availability, ETA and the normalised plan of a section edit |
| POST | /v1/aiwave/music-edit/lyrics/quote | no | Same, for a lyric line |
| POST | /v1/aiwave/music-edit/soundtrack/quote | no | Same, for a soundtrack |
| POST | /v1/aiwave/music-edit/section | yes | Regenerate a section |
| POST | /v1/aiwave/music-edit/lyrics | yes | Re-sing one line |
| POST | /v1/aiwave/music-edit/soundtrack | yes | Music for a video or an image |
| GET | /v1/aiwave/music-edit/jobs | no | Your history, newest first (?op=section|lyrics|soundtrack, ?limit=) |
| GET | /v1/aiwave/music-edit/jobs/{job_id} | no | Status, stage, result or error, refund state |
| POST | /v1/aiwave/music-edit/jobs/{job_id}/cancel | no | Cancel; 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→pricesmaps each operation to its price key: section and lyric edits useaiw-music-edit, soundtracks useaiw-soundtrack.- Each quote returns
credits,usd,available,eta_s,queue_depthandpayment. - 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-*answers404for its keys.
Section edit
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"],
)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);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"
}'| Field | Type | Description |
|---|---|---|
source_path | string | Required. 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_s | number | Required. Checked against the file; a mismatch beyond a small tolerance is refused |
start_s, end_s | number | Required. The region, between limits.min_section_s and limits.max_section_s long |
instruction | string | Required, up to limits.max_instruction_chars characters, Polish or English |
lyrics | string | The 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 |
style | string | The song's style, for example its original generation prompt |
snap_to_beat | bool | Default true: the region edges snap to the beat grid; the answer reports what was snapped |
crossfade_ms | integer | Seam cross-fade, within limits.crossfade_ms.min to .max (default from the config) |
seed | integer | |
parent_job_id | UUID | The job this edit builds on |
request_id | UUID | Idempotency 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).
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).
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;progressfrom 0 to 1;etawhile it waits.- Section and lyric edits are queued: they run one at a time on the music engine, so
etaandqueue_depthcan 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_urlclips 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 .../cancelrefunds 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_pathas the nextsource_pathand passparent_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().residencyis 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
| Status | error | Meaning |
|---|---|---|
| 401 | authentication_required | No or invalid key |
| 402 | insufficient_funds | Wallet too low; required_usd, balance_usd, shortfall_usd, topup_url |
| 403 | feature_disabled | Not enabled for your account yet |
| 404 | job_not_found | Not yours or gone |
| 409 | not_cancellable | The job has already finished |
| 422 | invalid_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_duration | Fixable input; nothing charged |
| 429 | ceiling_exceeded | kind: user_concurrency, daily_credits, rate; Retry-After; nothing charged |
| 503 | backend_unavailable | The 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.
Related
- Music & Audio: generate a song from scratch
- Video Edit by Chat: edit the clip, then score it
- Billing & Pricing and
GET /v1/pricing - Python SDK and TypeScript SDK

