Fix LLM Cost: accumulate per-version costs, include stems, show total

- Webhook no longer overwrites cost estimate with 0 when conversion_cost
  is missing from the payload
- Per-version costs (A/B) are tracked separately and summed into total
- Stems cost is only overwritten when a non-zero cost is reported
- Template shows combined total (songs + stems) with breakdown
- Added musicgpt_cost_a and musicgpt_cost_b columns to schema
This commit is contained in:
Troll (Hermes Agent) 2026-08-11 19:42:28 +00:00
parent f2c64848dd
commit 24838c73ac
3 changed files with 38 additions and 8 deletions

31
app.py
View file

@ -364,10 +364,29 @@ def musicgpt_webhook():
# the webhook is the rare complete one. # the webhook is the rare complete one.
if data.get('album_cover_path'): if data.get('album_cover_path'):
update_fields['album_cover_url'] = data.get('album_cover_path') update_fields['album_cover_url'] = data.get('album_cover_path')
# Accumulate cost from each per-version webhook instead of overwriting.
# The estimate is set when the job is queued; webhooks may report
# per-conversion costs. We sum them into the total.
try: try:
update_fields['musicgpt_cost'] = float(data.get('conversion_cost') or 0) cc = float(data.get('conversion_cost') or 0)
except (ValueError, TypeError): except (ValueError, TypeError):
pass cc = 0
if cc > 0:
# Only accumulate if the webhook reports a non-zero cost;
# otherwise keep whatever was already stored (estimate or prior sum).
current = float(req.get('musicgpt_cost') or 0)
# If this is a per-version webhook, add to the total.
# First webhook for version A: total = cc. Second for B: total += cc.
# If the estimate was stored and no per-version cost was reported yet,
# the first real cost replaces the estimate.
if version == 'A':
update_fields['musicgpt_cost'] = cc + float(req.get('musicgpt_cost_b') or 0)
update_fields['musicgpt_cost_a'] = cc
elif version == 'B':
update_fields['musicgpt_cost'] = float(req.get('musicgpt_cost_a') or 0) + cc
update_fields['musicgpt_cost_b'] = cc
else:
update_fields['musicgpt_cost'] = cc
download_musicgpt_outputs(req, data, version=version) download_musicgpt_outputs(req, data, version=version)
row = db.execute('SELECT * FROM requests WHERE id = ?', (req['id'],)).fetchone() row = db.execute('SELECT * FROM requests WHERE id = ?', (req['id'],)).fetchone()
req = dict(row) req = dict(row)
@ -402,9 +421,11 @@ def musicgpt_webhook():
update_fields['stems_url'] = stems_url update_fields['stems_url'] = stems_url
update_fields['stems_link'] = stems_url update_fields['stems_link'] = stems_url
try: try:
update_fields['stems_cost'] = float(data.get('conversion_cost') or 0) sc = float(data.get('conversion_cost') or 0)
except (ValueError, TypeError): except (ValueError, TypeError):
pass sc = 0
if sc > 0:
update_fields['stems_cost'] = sc
elif new_status in ('FAILED', 'ERROR'): elif new_status in ('FAILED', 'ERROR'):
update_fields['stems_error'] = data.get('reason') or data.get('error') or 'Extraction failed' update_fields['stems_error'] = data.get('reason') or data.get('error') or 'Extraction failed'
update_request(req['id'], **update_fields) update_request(req['id'], **update_fields)
@ -1537,7 +1558,7 @@ def admin_settings():
'approval_notified_at', 'preview_sent_at', 'delivery_sent_at', 'approval_notified_at', 'preview_sent_at', 'delivery_sent_at',
'square_payment_ref', 'admin_alert_email', 'player_token', 'revision_note', 'revision_count', 'operator_notes', 'stems_link', 'stems_interest', 'square_payment_ref', 'admin_alert_email', 'player_token', 'revision_note', 'revision_count', 'operator_notes', 'stems_link', 'stems_interest',
'musicgpt_task_id', 'musicgpt_conversion_id_1', 'musicgpt_conversion_id_2', 'musicgpt_task_id', 'musicgpt_conversion_id_1', 'musicgpt_conversion_id_2',
'musicgpt_status', 'musicgpt_cost', 'musicgpt_error', 'musicgpt_status', 'musicgpt_cost', 'musicgpt_cost_a', 'musicgpt_cost_b', 'musicgpt_error',
'album_cover_url', 'song_a_wav_path', 'song_b_wav_path', 'deliver_wav', 'album_cover_url', 'song_a_wav_path', 'song_b_wav_path', 'deliver_wav',
'stems_task_id', 'stems_status', 'stems_cost', 'stems_url', 'stems_error' 'stems_task_id', 'stems_status', 'stems_cost', 'stems_url', 'stems_error'
} }

View file

@ -60,7 +60,9 @@ CREATE TABLE IF NOT EXISTS requests (
musicgpt_conversion_id_1 TEXT, -- conversion ID for Version A musicgpt_conversion_id_1 TEXT, -- conversion ID for Version A
musicgpt_conversion_id_2 TEXT, -- conversion ID for Version B musicgpt_conversion_id_2 TEXT, -- conversion ID for Version B
musicgpt_status TEXT, -- IN_QUEUE / IN_PROGRESS / COMPLETED / FAILED musicgpt_status TEXT, -- IN_QUEUE / IN_PROGRESS / COMPLETED / FAILED
musicgpt_cost REAL, -- API-reported cost in USD credits musicgpt_cost REAL, -- total API-reported cost in USD credits (A + B)
musicgpt_cost_a REAL, -- per-version cost for Version A
musicgpt_cost_b REAL, -- per-version cost for Version B
musicgpt_error TEXT, -- error message from MusicGPT or polling musicgpt_error TEXT, -- error message from MusicGPT or polling
album_cover_url TEXT, -- URL to generated album cover image album_cover_url TEXT, -- URL to generated album cover image
song_a_wav_path TEXT, -- local path to Version A WAV (if deliver_wav) song_a_wav_path TEXT, -- local path to Version A WAV (if deliver_wav)
@ -126,7 +128,7 @@ def init_db():
'delivery_sent_at', 'square_payment_ref', 'admin_alert_email', 'delivery_sent_at', 'square_payment_ref', 'admin_alert_email',
'player_token', 'revision_count', 'revision_note', 'operator_notes', 'player_token', 'revision_count', 'revision_note', 'operator_notes',
'stems_link', 'stems_interest', 'musicgpt_task_id', 'musicgpt_conversion_id_1', 'stems_link', 'stems_interest', 'musicgpt_task_id', 'musicgpt_conversion_id_1',
'musicgpt_conversion_id_2', 'musicgpt_status', 'musicgpt_cost', 'musicgpt_error', 'musicgpt_conversion_id_2', 'musicgpt_status', 'musicgpt_cost', 'musicgpt_cost_a', 'musicgpt_cost_b', 'musicgpt_error',
'album_cover_url', 'song_a_wav_path', 'song_b_wav_path', 'deliver_wav', 'album_cover_url', 'song_a_wav_path', 'song_b_wav_path', 'deliver_wav',
'stems_task_id', 'stems_status', 'stems_cost', 'stems_url', 'stems_error' 'stems_task_id', 'stems_status', 'stems_cost', 'stems_url', 'stems_error'
], ],

View file

@ -294,7 +294,14 @@
</li> </li>
{% endfor %} {% endfor %}
</ol> </ol>
{% if req.musicgpt_cost is not none %} {% set total_cost = (req.musicgpt_cost or 0) + (req.stems_cost or 0) %}
{% if total_cost > 0 %}
<p style="margin:.5rem 0 0 0"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(total_cost) }}
{% if req.stems_cost %}
<span style="font-size:.75rem;color:#9ca3af"> (songs: {{ format_musicgpt_cost(req.musicgpt_cost) }} + stems: {{ format_musicgpt_cost(req.stems_cost) }})</span>
{% endif %}
</p>
{% elif req.musicgpt_cost is not none %}
<p style="margin:.5rem 0 0 0"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(req.musicgpt_cost) }}</p> <p style="margin:.5rem 0 0 0"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(req.musicgpt_cost) }}</p>
{% endif %} {% endif %}
{% endif %} {% endif %}