Better progression stats during generation from web

parent cb542996
...@@ -797,6 +797,10 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -797,6 +797,10 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div id="vi-dialog-section"></div> <div id="vi-dialog-section"></div>
<button class="btn btn-primary" onclick="genVideo('i2v')">Animate</button> <button class="btn btn-primary" onclick="genVideo('i2v')">Animate</button>
<div class="progress" id="vi-prog"></div> <div class="progress" id="vi-prog"></div>
<div class="gen-progress-wrap" id="vi-pbar-wrap">
<div class="gen-progress-bar-bg"><div class="gen-progress-bar-fill" id="vi-pbar-fill"></div></div>
<div class="gen-progress-label" id="vi-pbar-label"></div>
</div>
</div> </div>
<div class="gen-out" id="vi-out"><div class="gen-empty">Animated video will appear here</div></div> <div class="gen-out" id="vi-out"><div class="gen-empty">Animated video will appear here</div></div>
</div></div> </div></div>
...@@ -842,6 +846,10 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -842,6 +846,10 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div id="vv-dialog-section"></div> <div id="vv-dialog-section"></div>
<button class="btn btn-primary" onclick="genVideo('v2v')">Transform</button> <button class="btn btn-primary" onclick="genVideo('v2v')">Transform</button>
<div class="progress" id="vv-prog"></div> <div class="progress" id="vv-prog"></div>
<div class="gen-progress-wrap" id="vv-pbar-wrap">
<div class="gen-progress-bar-bg"><div class="gen-progress-bar-fill" id="vv-pbar-fill"></div></div>
<div class="gen-progress-label" id="vv-pbar-label"></div>
</div>
</div> </div>
<div class="gen-out" id="vv-out"><div class="gen-empty">Transformed video will appear here</div></div> <div class="gen-out" id="vv-out"><div class="gen-empty">Transformed video will appear here</div></div>
</div></div> </div></div>
...@@ -945,6 +953,10 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -945,6 +953,10 @@ a.dl { display:inline-block; margin-top:.4rem; }
</details> </details>
<button class="btn btn-primary" onclick="genTi2V()" style="margin-top:.25rem">Generate</button> <button class="btn btn-primary" onclick="genTi2V()" style="margin-top:.25rem">Generate</button>
<div class="progress" id="ti-prog"></div> <div class="progress" id="ti-prog"></div>
<div class="gen-progress-wrap" id="ti-pbar-wrap">
<div class="gen-progress-bar-bg"><div class="gen-progress-bar-fill" id="ti-pbar-fill"></div></div>
<div class="gen-progress-label" id="ti-pbar-label"></div>
</div>
</div> </div>
<div class="gen-out" id="ti-out"><div class="gen-empty">Video will appear here</div></div> <div class="gen-out" id="ti-out"><div class="gen-empty">Video will appear here</div></div>
</div></div> </div></div>
...@@ -1100,6 +1112,10 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -1100,6 +1112,10 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="req-preview" id="ag-preview"></div> <div class="req-preview" id="ag-preview"></div>
<button class="btn btn-primary" onclick="genAudio()">Generate</button> <button class="btn btn-primary" onclick="genAudio()">Generate</button>
<div class="progress" id="ag-prog"></div> <div class="progress" id="ag-prog"></div>
<div class="gen-progress-wrap" id="ag-pbar-wrap">
<div class="gen-progress-bar-bg"><div class="gen-progress-bar-fill" id="ag-pbar-fill"></div></div>
<div class="gen-progress-label" id="ag-pbar-label"></div>
</div>
</div> </div>
<div class="gen-out" id="ag-out"><div class="gen-empty">Generated audio will appear here</div></div> <div class="gen-out" id="ag-out"><div class="gen-empty">Generated audio will appear here</div></div>
</div> </div>
...@@ -2067,6 +2083,65 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -2067,6 +2083,65 @@ a.dl { display:inline-block; margin-top:.4rem; }
// ───────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────
let models = [], activeModel = null, chatHistory = [], chatBusy = false, attachedImage = null; let models = [], activeModel = null, chatHistory = [], chatBusy = false, attachedImage = null;
let _imgPollTimer = null; let _imgPollTimer = null;
let _vidPollTimer = null;
let _audPollTimer = null;
function _startVidPoll(prefix) {
if (_vidPollTimer) { clearInterval(_vidPollTimer); _vidPollTimer = null; }
const wrap = $(prefix+'-pbar-wrap'), fill = $(prefix+'-pbar-fill'), lbl = $(prefix+'-pbar-label');
if (!wrap) return;
wrap.classList.add('active'); fill.style.width='0%'; lbl.textContent='';
_vidPollTimer = setInterval(async () => {
try {
const p = await (await fetch('/v1/video/progress')).json();
if (p.total > 0) {
fill.style.width = p.pct + '%';
const spd = p.it_per_s > 0 ? ` · ${p.it_per_s} it/s` : (p.elapsed > 0 ? ` · ${p.elapsed}s` : '');
lbl.textContent = `${p.current} / ${p.total} steps${spd}`;
} else if (p.elapsed > 0) {
lbl.textContent = `${p.elapsed}s`;
}
if (!p.active) { clearInterval(_vidPollTimer); _vidPollTimer = null; }
} catch(_) {}
}, 500);
}
function _stopVidPoll(prefix, done) {
if (_vidPollTimer) { clearInterval(_vidPollTimer); _vidPollTimer = null; }
const wrap = $(prefix+'-pbar-wrap'), fill = $(prefix+'-pbar-fill'), lbl = $(prefix+'-pbar-label');
if (!wrap) return;
if (done) { fill.style.width='100%'; lbl.textContent='Done'; setTimeout(() => wrap.classList.remove('active'), 2000); }
else { wrap.classList.remove('active'); }
}
function _startAudPoll(prefix) {
if (_audPollTimer) { clearInterval(_audPollTimer); _audPollTimer = null; }
const wrap = $(prefix+'-pbar-wrap'), fill = $(prefix+'-pbar-fill'), lbl = $(prefix+'-pbar-label');
if (!wrap) return;
wrap.classList.add('active'); fill.style.width='0%'; lbl.textContent='';
_audPollTimer = setInterval(async () => {
try {
const p = await (await fetch('/v1/audio/progress')).json();
if (p.total > 0) {
fill.style.width = p.pct + '%';
const unit = p.unit || 'it';
const spd = p.it_per_s > 0 ? ` · ${p.it_per_s} ${unit}/s` : (p.elapsed > 0 ? ` · ${p.elapsed}s` : '');
lbl.textContent = `${p.current} / ${p.total} steps${spd}`;
} else if (p.elapsed > 0) {
lbl.textContent = `Elapsed: ${p.elapsed}s`;
}
if (!p.active) { clearInterval(_audPollTimer); _audPollTimer = null; }
} catch(_) {}
}, 500);
}
function _stopAudPoll(prefix, done) {
if (_audPollTimer) { clearInterval(_audPollTimer); _audPollTimer = null; }
const wrap = $(prefix+'-pbar-wrap'), fill = $(prefix+'-pbar-fill'), lbl = $(prefix+'-pbar-label');
if (!wrap) return;
if (done) { fill.style.width='100%'; lbl.textContent='Done'; setTimeout(() => wrap.classList.remove('active'), 2000); }
else { wrap.classList.remove('active'); }
}
let apiToken = null; let apiToken = null;
let charSlots = {}; // prefix → [{name:'', images:[b64...]}] let charSlots = {}; // prefix → [{name:'', images:[b64...]}]
let _charProfiles = []; // cached list from /v1/characters let _charProfiles = []; // cached list from /v1/characters
...@@ -3543,6 +3618,7 @@ async function sendChat() { ...@@ -3543,6 +3618,7 @@ async function sendChat() {
attachedImage = null; updateAttachBar(); attachedImage = null; updateAttachBar();
chatBusy = true; $('send-btn').disabled = true; chatBusy = true; $('send-btn').disabled = true;
$('typing').textContent = 'Thinking…'; $('typing').textContent = 'Thinking…';
const _chatT0 = Date.now();
try { try {
const r = await fetch('/v1/chat/completions',{ const r = await fetch('/v1/chat/completions',{
method:'POST',headers:{'Content-Type':'application/json'}, method:'POST',headers:{'Content-Type':'application/json'},
...@@ -3551,10 +3627,15 @@ async function sendChat() { ...@@ -3551,10 +3627,15 @@ async function sendChat() {
if (!r.ok) throw new Error('HTTP '+r.status+': '+await r.text()); if (!r.ok) throw new Error('HTTP '+r.status+': '+await r.text());
const d = await r.json(); const d = await r.json();
const reply = d.choices[0].message.content; const reply = d.choices[0].message.content;
const elapsed = (Date.now() - _chatT0) / 1000;
const toks = d.usage?.completion_tokens;
if (toks && elapsed > 0) {
$('typing').textContent = `${toks} tok · ${(toks/elapsed).toFixed(1)} tok/s`;
}
addMsg('assistant',reply); addMsg('assistant',reply);
chatHistory.push({role:'assistant',content:reply}); chatHistory.push({role:'assistant',content:reply});
} catch(e) { addMsg('assistant','Error: '+e.message); } } catch(e) { addMsg('assistant','Error: '+e.message); }
finally { chatBusy=false; $('send-btn').disabled=false; $('typing').textContent=''; } finally { chatBusy=false; $('send-btn').disabled=false; setTimeout(()=>{ $('typing').textContent=''; }, 3000); }
} }
$('chat-in').addEventListener('keydown', e => { if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendChat();} }); $('chat-in').addEventListener('keydown', e => { if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendChat();} });
...@@ -3977,7 +4058,8 @@ async function genImage() { ...@@ -3977,7 +4058,8 @@ async function genImage() {
const p=await (await fetch('/v1/images/progress')).json(); const p=await (await fetch('/v1/images/progress')).json();
if(p.total>0){ if(p.total>0){
fill.style.width=p.pct+'%'; fill.style.width=p.pct+'%';
lbl.textContent=p.current+' / '+p.total+' steps'; const spd = p.it_per_s>0 ? ` · ${p.it_per_s} it/s` : (p.elapsed>0 ? ` · ${p.elapsed}s` : '');
lbl.textContent=`${p.current} / ${p.total} steps${spd}`;
} }
if(!p.active){ clearInterval(_imgPollTimer); _imgPollTimer=null; } if(!p.active){ clearInterval(_imgPollTimer); _imgPollTimer=null; }
}catch(_){} }catch(_){}
...@@ -4104,10 +4186,12 @@ async function genSegment() { ...@@ -4104,10 +4186,12 @@ async function genSegment() {
// ───────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────
async function genVideo(mode) { async function genVideo(mode) {
if (!activeModel) return; if (!activeModel) return;
const progMap = {t2v:'vt-prog', i2v:'vi-prog', v2v:'vv-prog'}; const progMap = {t2v:'vt-prog', i2v:'vi-prog', v2v:'vv-prog'};
const outMap = {t2v:'vt-out', i2v:'vi-out', v2v:'vv-out'}; const outMap = {t2v:'vt-out', i2v:'vi-out', v2v:'vv-out'};
const prog = progMap[mode], outId = outMap[mode]; const prefixMap = {t2v:'vt', i2v:'vi', v2v:'vv'};
const prog = progMap[mode], outId = outMap[mode], prefix = prefixMap[mode];
$(prog).textContent='Generating… (this may take several minutes)'; $(prog).textContent='Generating… (this may take several minutes)';
_startVidPoll(prefix);
const subId = {t2v:'vid-t2v', i2v:'vid-i2v', v2v:'vid-v2v'}[mode]; const subId = {t2v:'vid-t2v', i2v:'vid-i2v', v2v:'vid-v2v'}[mode];
const body = {model:modelForSub(subId), mode}; const body = {model:modelForSub(subId), mode};
if (mode==='t2v') { if (mode==='t2v') {
...@@ -4160,8 +4244,9 @@ async function genVideo(mode) { ...@@ -4160,8 +4244,9 @@ async function genVideo(mode) {
} }
try { try {
const d = await post('/v1/video/generations', body); const d = await post('/v1/video/generations', body);
_stopVidPoll(prefix, true);
showVideo(outId, vidSrc(d.data[0]), prog); showVideo(outId, vidSrc(d.data[0]), prog);
} catch(e) { $(prog).textContent='Error: '+e.message; } } catch(e) { _stopVidPoll(prefix, false); $(prog).textContent='Error: '+e.message; }
} }
// ───────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────
...@@ -4172,6 +4257,7 @@ async function genTi2V() { ...@@ -4172,6 +4257,7 @@ async function genTi2V() {
$('ti-prog').textContent='Generating… (may take several minutes)'; $('ti-prog').textContent='Generating… (may take several minutes)';
const [initImg, endImg, srcVid] = await Promise.all([b64OrNull('ti-init'), b64OrNull('ti-end'), b64OrNull('ti-vid')]); const [initImg, endImg, srcVid] = await Promise.all([b64OrNull('ti-init'), b64OrNull('ti-end'), b64OrNull('ti-vid')]);
if (!srcVid && !initImg) { $('ti-prog').textContent='Select an initial image or source video.'; return; } if (!srcVid && !initImg) { $('ti-prog').textContent='Select an initial image or source video.'; return; }
_startVidPoll('ti');
const body = { const body = {
model:modelForSub('vid-ti2v'), model:modelForSub('vid-ti2v'),
...@@ -4217,8 +4303,9 @@ async function genTi2V() { ...@@ -4217,8 +4303,9 @@ async function genTi2V() {
try { try {
const d = await post('/v1/video/generations', body); const d = await post('/v1/video/generations', body);
_stopVidPoll('ti', true);
showVideo('ti-out', vidSrc(d.data[0]), 'ti-prog'); showVideo('ti-out', vidSrc(d.data[0]), 'ti-prog');
} catch(e) { $('ti-prog').textContent='Error: '+e.message; } } catch(e) { _stopVidPoll('ti', false); $('ti-prog').textContent='Error: '+e.message; }
} }
// ───────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────
...@@ -4928,6 +5015,7 @@ async function runPipeline4() { ...@@ -4928,6 +5015,7 @@ async function runPipeline4() {
async function genAudio() { async function genAudio() {
if (!activeModel) return; if (!activeModel) return;
$('ag-prog').textContent='Generating audio…'; $('ag-prog').textContent='Generating audio…';
_startAudPoll('ag');
const melody = await b64OrNull('ag-melody'); const melody = await b64OrNull('ag-melody');
const body = { const body = {
model:modelForSub('aud-gen'), prompt:val('ag-prompt'), model:modelForSub('aud-gen'), prompt:val('ag-prompt'),
...@@ -4939,6 +5027,7 @@ async function genAudio() { ...@@ -4939,6 +5027,7 @@ async function genAudio() {
}; };
try { try {
const d = await post('/v1/audio/generate', body); const d = await post('/v1/audio/generate', body);
_stopAudPoll('ag', true);
const item = d.data[0]; const item = d.data[0];
const src = audSrc(item); const src = audSrc(item);
showAudio('ag-out', src, 'ag-prog', 'wav'); showAudio('ag-out', src, 'ag-prog', 'wav');
...@@ -4949,7 +5038,7 @@ async function genAudio() { ...@@ -4949,7 +5038,7 @@ async function genAudio() {
summary:buildAudioHistorySummary(val('ag-prompt'), body.duration), summary:buildAudioHistorySummary(val('ag-prompt'), body.duration),
links:buildAudioLinks(item), links:buildAudioLinks(item),
}); });
} catch(e) { $('ag-prog').textContent='Error: '+e.message; } } catch(e) { _stopAudPoll('ag', false); $('ag-prog').textContent='Error: '+e.message; }
} }
// ───────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────
...@@ -5289,6 +5378,10 @@ document.getElementById('panel-vid-t2v').innerHTML = `<div class="gen-wrap"> ...@@ -5289,6 +5378,10 @@ document.getElementById('panel-vid-t2v').innerHTML = `<div class="gen-wrap">
<div id="vt-dialog-section"></div> <div id="vt-dialog-section"></div>
<button class="btn btn-primary" onclick="genVideo('t2v')">Generate Video</button> <button class="btn btn-primary" onclick="genVideo('t2v')">Generate Video</button>
<div class="progress" id="vt-prog"></div> <div class="progress" id="vt-prog"></div>
<div class="gen-progress-wrap" id="vt-pbar-wrap">
<div class="gen-progress-bar-bg"><div class="gen-progress-bar-fill" id="vt-pbar-fill"></div></div>
<div class="gen-progress-label" id="vt-pbar-label"></div>
</div>
</div> </div>
<div class="gen-out" id="vt-out"><div class="gen-empty">Video will appear here</div></div> <div class="gen-out" id="vt-out"><div class="gen-empty">Video will appear here</div></div>
</div>`; </div>`;
......
...@@ -37,6 +37,32 @@ router = APIRouter() ...@@ -37,6 +37,32 @@ router = APIRouter()
global_args = None global_args = None
global_file_path = None global_file_path = None
# =============================================================================
# Audio generation progress tracking
# =============================================================================
_aud_progress: dict = {
"current": 0, "total": 0, "active": False,
"started_at": 0.0, "it_per_s": 0.0, "unit": "it",
}
def _aud_progress_reset(total: int, unit: str = "it"):
_aud_progress["current"] = 0
_aud_progress["total"] = total
_aud_progress["active"] = True
_aud_progress["started_at"] = time.monotonic()
_aud_progress["it_per_s"] = 0.0
_aud_progress["unit"] = unit
def _aud_progress_done():
_aud_progress["current"] = max(_aud_progress["current"], _aud_progress["total"])
_aud_progress["active"] = False
def _aud_progress_step(step: int):
_aud_progress["current"] = step
elapsed = time.monotonic() - _aud_progress["started_at"]
if elapsed > 0 and step > 0:
_aud_progress["it_per_s"] = round(step / elapsed, 2)
def set_global_args(args): def set_global_args(args):
global global_args global global_args
...@@ -124,6 +150,8 @@ def _generate_audio(pipe, model_name: str, request: AudioGenerationRequest): ...@@ -124,6 +150,8 @@ def _generate_audio(pipe, model_name: str, request: AudioGenerationRequest):
temperature=request.temperature, temperature=request.temperature,
cfg_coef=request.cfg_coef, cfg_coef=request.cfg_coef,
) )
# MusicGen/AudioGen generate in one shot — track elapsed only
_aud_progress_reset(0, unit="s")
if request.melody and model_type == 'musicgen': if request.melody and model_type == 'musicgen':
import torchaudio, torch import torchaudio, torch
raw = _decode_b64_or_url(request.melody) raw = _decode_b64_or_url(request.melody)
...@@ -135,10 +163,18 @@ def _generate_audio(pipe, model_name: str, request: AudioGenerationRequest): ...@@ -135,10 +163,18 @@ def _generate_audio(pipe, model_name: str, request: AudioGenerationRequest):
sr = pipe.sample_rate sr = pipe.sample_rate
elif model_type == 'audioldm': elif model_type == 'audioldm':
num_steps = 50
_aud_progress_reset(num_steps, unit="it")
def _aud_step_cb(pipe, step_index, timestep, callback_kwargs):
_aud_progress_step(step_index + 1)
return callback_kwargs
result = pipe( result = pipe(
request.prompt, request.prompt,
num_inference_steps=50, num_inference_steps=num_steps,
audio_length_in_s=request.duration, audio_length_in_s=request.duration,
callback_on_step_end=_aud_step_cb,
) )
audio_np = result.audios[0] audio_np = result.audios[0]
sr = 16000 sr = 16000
...@@ -162,6 +198,23 @@ def _decode_b64_or_url(data: str) -> bytes: ...@@ -162,6 +198,23 @@ def _decode_b64_or_url(data: str) -> bytes:
return base64.b64decode(data) return base64.b64decode(data)
@router.get("/v1/audio/progress")
async def get_audio_progress():
"""Return current audio generation progress including speed."""
elapsed = time.monotonic() - _aud_progress["started_at"] if _aud_progress["active"] else 0.0
total = _aud_progress["total"]
current = _aud_progress["current"]
return {
"current": current,
"total": total,
"active": _aud_progress["active"],
"pct": int(current / total * 100) if total > 0 else 0,
"it_per_s": _aud_progress["it_per_s"],
"elapsed": round(elapsed, 1),
"unit": _aud_progress["unit"],
}
@router.post("/v1/audio/generate", response_model=AudioGenerationResponse) @router.post("/v1/audio/generate", response_model=AudioGenerationResponse)
async def audio_generate(request: AudioGenerationRequest, http_request: Request = None): async def audio_generate(request: AudioGenerationRequest, http_request: Request = None):
""" """
...@@ -196,7 +249,10 @@ async def audio_generate(request: AudioGenerationRequest, http_request: Request ...@@ -196,7 +249,10 @@ async def audio_generate(request: AudioGenerationRequest, http_request: Request
audio_bytes, ext = await asyncio.get_event_loop().run_in_executor( audio_bytes, ext = await asyncio.get_event_loop().run_in_executor(
None, _generate_audio, pipe, model_name, request) None, _generate_audio, pipe, model_name, request)
except Exception as e: except Exception as e:
_aud_progress_done()
raise HTTPException(status_code=500, detail=f"Audio generation failed: {e}") raise HTTPException(status_code=500, detail=f"Audio generation failed: {e}")
finally:
_aud_progress_done()
result = _save_audio_response(audio_bytes, ext, http_request) result = _save_audio_response(audio_bytes, ext, http_request)
......
...@@ -118,12 +118,19 @@ queue_flags = {} ...@@ -118,12 +118,19 @@ queue_flags = {}
# ============================================================================= # =============================================================================
# Generation progress tracking # Generation progress tracking
# ============================================================================= # =============================================================================
_gen_progress: dict = {"current": 0, "total": 0, "active": False} import time as _time
_gen_progress: dict = {
"current": 0, "total": 0, "active": False,
"started_at": 0.0, "it_per_s": 0.0,
}
def _progress_reset(total: int): def _progress_reset(total: int):
_gen_progress["current"] = 0 _gen_progress["current"] = 0
_gen_progress["total"] = total _gen_progress["total"] = total
_gen_progress["active"] = True _gen_progress["active"] = True
_gen_progress["started_at"] = _time.monotonic()
_gen_progress["it_per_s"] = 0.0
def _progress_done(): def _progress_done():
_gen_progress["current"] = _gen_progress["total"] _gen_progress["current"] = _gen_progress["total"]
...@@ -131,6 +138,9 @@ def _progress_done(): ...@@ -131,6 +138,9 @@ def _progress_done():
def _progress_step(step: int): def _progress_step(step: int):
_gen_progress["current"] = step _gen_progress["current"] = step
elapsed = _time.monotonic() - _gen_progress["started_at"]
if elapsed > 0 and step > 0:
_gen_progress["it_per_s"] = round(step / elapsed, 2)
# ============================================================================= # =============================================================================
...@@ -884,13 +894,16 @@ router = APIRouter() ...@@ -884,13 +894,16 @@ router = APIRouter()
@router.get("/v1/images/progress") @router.get("/v1/images/progress")
async def get_image_progress(): async def get_image_progress():
"""Return current image generation step progress.""" """Return current image generation step progress including speed."""
elapsed = _time.monotonic() - _gen_progress["started_at"] if _gen_progress["active"] else 0.0
return { return {
"current": _gen_progress["current"], "current": _gen_progress["current"],
"total": _gen_progress["total"], "total": _gen_progress["total"],
"active": _gen_progress["active"], "active": _gen_progress["active"],
"pct": int(_gen_progress["current"] / _gen_progress["total"] * 100) "pct": int(_gen_progress["current"] / _gen_progress["total"] * 100)
if _gen_progress["total"] > 0 else 0, if _gen_progress["total"] > 0 else 0,
"it_per_s": _gen_progress["it_per_s"],
"elapsed": round(elapsed, 1),
} }
......
...@@ -51,6 +51,31 @@ router = APIRouter() ...@@ -51,6 +51,31 @@ router = APIRouter()
global_args = None global_args = None
global_file_path = None global_file_path = None
# =============================================================================
# Video generation progress tracking
# =============================================================================
_vid_progress: dict = {
"current": 0, "total": 0, "active": False,
"started_at": 0.0, "it_per_s": 0.0,
}
def _vid_progress_reset(total: int):
_vid_progress["current"] = 0
_vid_progress["total"] = total
_vid_progress["active"] = True
_vid_progress["started_at"] = time.monotonic()
_vid_progress["it_per_s"] = 0.0
def _vid_progress_done():
_vid_progress["current"] = _vid_progress["total"]
_vid_progress["active"] = False
def _vid_progress_step(step: int):
_vid_progress["current"] = step
elapsed = time.monotonic() - _vid_progress["started_at"]
if elapsed > 0 and step > 0:
_vid_progress["it_per_s"] = round(step / elapsed, 2)
def set_global_args(args): def set_global_args(args):
global global_args global global_args
...@@ -322,6 +347,17 @@ def _generate_video(pipe, request: VideoGenerationRequest): ...@@ -322,6 +347,17 @@ def _generate_video(pipe, request: VideoGenerationRequest):
kw.setdefault('guidance_scale', 7.5) kw.setdefault('guidance_scale', 7.5)
kw.setdefault('num_frames', 16) kw.setdefault('num_frames', 16)
_vid_progress_reset(kw['num_inference_steps'])
def _vid_step_cb(pipe, step_index, timestep, callback_kwargs):
_vid_progress_step(step_index + 1)
return callback_kwargs
try:
kw['callback_on_step_end'] = _vid_step_cb
except Exception:
pass
_apply_camera_motion(kw, request.camera_motion) _apply_camera_motion(kw, request.camera_motion)
char_images, char_names = _resolve_character_inputs(request) char_images, char_names = _resolve_character_inputs(request)
...@@ -355,6 +391,7 @@ def _generate_video(pipe, request: VideoGenerationRequest): ...@@ -355,6 +391,7 @@ def _generate_video(pipe, request: VideoGenerationRequest):
kw['strength'] = request.strength kw['strength'] = request.strength
frames = _run_pipeline(pipe, kw) frames = _run_pipeline(pipe, kw)
_vid_progress_done()
return frames, fps return frames, fps
...@@ -781,6 +818,25 @@ def _translate_srt(srt_path: str, target_lang: str, temps: list) -> str: ...@@ -781,6 +818,25 @@ def _translate_srt(srt_path: str, target_lang: str, temps: list) -> str:
return srt_path return srt_path
# =============================================================================
# Progress endpoint
# =============================================================================
@router.get("/v1/video/progress")
async def get_video_progress():
"""Return current video generation step progress including speed."""
elapsed = time.monotonic() - _vid_progress["started_at"] if _vid_progress["active"] else 0.0
return {
"current": _vid_progress["current"],
"total": _vid_progress["total"],
"active": _vid_progress["active"],
"pct": int(_vid_progress["current"] / _vid_progress["total"] * 100)
if _vid_progress["total"] > 0 else 0,
"it_per_s": _vid_progress["it_per_s"],
"elapsed": round(elapsed, 1),
}
# ============================================================================= # =============================================================================
# Main generation endpoint # Main generation endpoint
# ============================================================================= # =============================================================================
...@@ -836,6 +892,7 @@ async def video_generations(request: VideoGenerationRequest, ...@@ -836,6 +892,7 @@ async def video_generations(request: VideoGenerationRequest,
frames, fps = await asyncio.get_event_loop().run_in_executor( frames, fps = await asyncio.get_event_loop().run_in_executor(
None, _generate_video, pipe, request) None, _generate_video, pipe, request)
except Exception as e: except Exception as e:
_vid_progress_done()
raise HTTPException(status_code=500, detail=f"Video generation failed: {e}") raise HTTPException(status_code=500, detail=f"Video generation failed: {e}")
# Encode raw frames to MP4 # Encode raw frames to MP4
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment