Source code for mstar.api_server.openai.serving_videos

"""/v1/videos/generations (text-to-video and image-to-video) handler."""

from __future__ import annotations

import base64
import json
import logging

from mstar.api_server import media_io
from mstar.api_server.openai._util import now, rid

logger = logging.getLogger(__name__)


[docs] async def create_videos(api, model_name, adapter, req, raw_request=None): # noqa: ARG001 args = adapter.video_to_request(req, api.upload_dir) request_id = rid("vid") stream = bool(args.model_kwargs.get("stream_video")) try: api.submit_request( text=args.text, file_paths=args.file_paths, input_modalities=args.input_modalities, # the adapter decides: video alone, or video with its generated audio output_modalities=args.output_modalities, model_kwargs=args.model_kwargs, streaming=stream, request_id=request_id, ) if stream: return _stream_ndjson(api, request_id) chunks = await api.collect_results(request_id, raw_request) finally: media_io.schedule_upload_cleanup(args.file_paths, api.upload_dir) # Each video chunk is an mp4 (H.264); return it base64-encoded, mirroring the # image endpoint's b64_json shape. A sound request additionally emits a raw # 16-bit PCM audio chunk, which is muxed into the mp4 as an AAC track (one # playable file with sound); if the mux fails the plain video is returned. # The mux also rescales the video timestamps to the request frame rate: the # video-only container always plays at the model's default fps, which would # drift out of sync with the true-time audio track for other request rates. request_fps = args.model_kwargs.get("fps") videos = [c.data for c in chunks if c.modality == "video"] audios = [c for c in chunks if c.modality == "audio"] data = [] for i, video in enumerate(videos): if i < len(audios): audio = audios[i] try: video = media_io.mux_mp4_with_pcm16( video, audio.data, sample_rate=int((audio.metadata or {}).get("sample_rate", 48000)), num_channels=int((audio.metadata or {}).get("num_channels", 2)), video_fps=float(request_fps) if request_fps else None, ) except Exception: # noqa: BLE001 — degrade to video-only, keep serving logger.exception("Muxing generated audio into the mp4 failed; returning video only") data.append({"b64_json": base64.b64encode(video).decode("ascii"), "url": None}) return {"created": now(), "data": data}
def _stream_ndjson(api, request_id): # Bound to the submission now: the body is iterated later, by Starlette. return _ndjson_lines(api.iter_result_chunks(request_id)) async def _ndjson_lines(chunks): """Yield one NDJSON line per result chunk for a ``stream_video`` request. A windowed request emits each window's mp4 as its own ``video`` chunk; the lines use the ``/generate`` wire shape (modality / base64 data / metadata) with a running ``index``, and the stream is closed by a ``done`` line carrying the chunk count. Failures after the stream is open travel in-band as a terminal ``error`` line (the HTTP status is committed), in which case no ``done`` line follows. """ index = 0 failed = False async for chunk in chunks: metadata = dict(chunk.metadata or {}) if chunk.modality == "error": failed = True else: metadata["index"] = index index += 1 yield json.dumps({ "modality": chunk.modality, "data": base64.b64encode(chunk.data).decode("ascii"), "metadata": metadata, }) + "\n" if not failed: yield json.dumps( {"modality": "done", "data": "", "metadata": {"chunks": index}} ) + "\n"