studio.mjs
Rama main · réplica de lectura del repositorio original.
/** Strip trailing ASCII slashes without a regex (CodeQL js/polynomial-redos). */
function trimTrailingSlashes(s) {
let end = s.length;
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
return end === s.length ? s : s.slice(0, end);
}
// studio.mjs
// Complete Vivijure studio HTTP client (docs/CONTRACT.md). Slate is a thin projection client.
function buildUrl(base, path, query) {
const url = new URL(trimTrailingSlashes(base) + path);
if (query) {
for (const [k, v] of Object.entries(query)) {
if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v));
}
}
return url.toString();
}
async function readResponse(res) {
const ct = (res.headers.get('content-type') || '').toLowerCase();
const text = await res.text().catch(() => '');
if (ct.includes('json') && text) {
try {
return { ok: res.ok, status: res.status, data: JSON.parse(text), raw: text };
} catch {
return { ok: res.ok, status: res.status, data: null, raw: text };
}
}
return { ok: res.ok, status: res.status, data: text, raw: text };
}
// Observer hook (slate#90): every studio call flows through this one function, so it is the single
// choke point for a full traffic ledger without threading channel/session context through 100+ call
// sites. bot.mjs registers an observer that logs each request/response to D1 and, for mutating calls,
// indexes a summary into Vectorize for RAG. Kept optional and side-effect-free by default so studio.mjs
// stays a plain, bot-agnostic HTTP client (unit-testable, no D1/Vectorize awareness).
let requestObserver = null;
export function setStudioRequestObserver(fn) { requestObserver = fn; }
function notifyObserver(event) {
if (!requestObserver) return;
try { requestObserver(event); } catch { /* observer failures never affect the studio call */ }
}
export async function studioRequest(baseUrl, headers, method, path, opts = {}) {
const init = { method, headers: { ...headers } };
if (opts.body !== undefined && !(opts.body instanceof ArrayBuffer) && !ArrayBuffer.isView(opts.body)) {
if (typeof opts.body === 'string') {
init.body = opts.body;
} else {
init.headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(opts.body);
}
} else if (opts.body !== undefined) {
init.body = opts.body;
}
if (opts.contentType) init.headers['Content-Type'] = opts.contentType;
const startedAt = Date.now();
try {
const res = await fetch(buildUrl(baseUrl, path, opts.query), init);
const result = await readResponse(res);
notifyObserver({ method, path, query: opts.query, body: opts.body, result, latencyMs: Date.now() - startedAt });
return result;
} catch (e) {
notifyObserver({ method, path, query: opts.query, body: opts.body, error: e, latencyMs: Date.now() - startedAt });
throw e;
}
}
export async function studioGet(baseUrl, headers, path, query) {
return studioRequest(baseUrl, headers, 'GET', path, { query });
}
export async function studioPost(baseUrl, headers, path, body, opts = {}) {
return studioRequest(baseUrl, headers, 'POST', path, { body, ...opts });
}
export async function studioPatch(baseUrl, headers, path, body) {
return studioRequest(baseUrl, headers, 'PATCH', path, { body });
}
export async function studioDelete(baseUrl, headers, path, body) {
return studioRequest(baseUrl, headers, 'DELETE', path, body ? { body } : {});
}
export async function studioUploadBinary(baseUrl, headers, path, buffer, mime) {
return studioPost(baseUrl, headers, path, buffer, { contentType: mime });
}
// --- system -------------------------------------------------------------------
export const getHealth = (c) => studioGet(c.vivijureUrl, c.headers, '/health');
export const getModules = (c) => studioGet(c.vivijureUrl, c.headers, '/api/modules');
export const getWhoami = (c) => studioGet(c.vivijureUrl, c.headers, '/api/whoami');
export const getPrefs = (c) => studioGet(c.vivijureUrl, c.headers, '/api/prefs');
export const patchPrefs = (c, body) => studioPatch(c.vivijureUrl, c.headers, '/api/prefs', body);
export const getModuleInstallConfig = (c, name) =>
studioGet(c.vivijureUrl, c.headers, `/api/modules/${encodeURIComponent(name)}/config`);
export const patchModuleInstallConfig = (c, name, body) =>
studioPatch(c.vivijureUrl, c.headers, `/api/modules/${encodeURIComponent(name)}/config`, body);
// --- voices / models ----------------------------------------------------------
export const listVoices = (c) => studioGet(c.vivijureUrl, c.headers, '/api/voices');
export const getStoryboardModels = (c) => studioGet(c.vivijureUrl, c.headers, '/api/storyboard/models');
// --- cast ---------------------------------------------------------------------
export const listCast = (c) => studioGet(c.vivijureUrl, c.headers, '/api/cast');
export const getCast = (c, id) => studioGet(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}`);
export const createCast = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/cast', body);
export const updateCast = (c, id, body) => studioPatch(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}`, body);
export const deleteCast = (c, id) => studioDelete(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}`);
export const setCastPortrait = (c, id, body) =>
studioPost(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/portrait`, body);
export const deleteCastPortrait = (c, id) =>
studioDelete(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/portrait`);
export const addCastRef = (c, id, body) =>
studioPost(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/ref`, body);
export const deleteCastRef = (c, id, body) =>
studioDelete(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/ref`, body);
export const deleteCastRefByKey = (c, id, refKey) =>
studioDelete(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/refs/${encodeURIComponent(refKey)}`);
export const addCastSource = (c, id, body) =>
studioPost(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/source`, body);
export const deleteCastSource = (c, id, body) =>
studioDelete(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/source`, body);
export const deleteCastSourceByKey = (c, id, sourceKey) =>
studioDelete(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/source/${encodeURIComponent(sourceKey)}`);
export const generateCastRefs = (c, id, body = {}) =>
studioPost(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/generate-refs`, body);
export const pollCastRefsJob = (c, id, jobId) =>
studioGet(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/refs-job/${encodeURIComponent(jobId)}`);
export const trainCastLora = (c, id, body = {}) =>
studioPost(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/train-lora`, body);
export const getCastLoraStatus = (c, id) =>
studioGet(c.vivijureUrl, c.headers, `/api/cast/${encodeURIComponent(id)}/lora-status`);
export const exportCastUrl = (c, id) => `${trimTrailingSlashes(c.vivijureUrl)}/api/cast/export/${encodeURIComponent(id)}`;
export const importCast = (c, buffer) =>
studioUploadBinary(c.vivijureUrl, c.headers, '/api/cast/import', buffer, 'application/x-tar');
// --- uploads ------------------------------------------------------------------
export const uploadImage = (c, buffer, mime) =>
studioUploadBinary(c.vivijureUrl, c.headers, '/api/upload', buffer, mime);
export const uploadCharacterRef = (c, buffer, mime) =>
studioUploadBinary(c.vivijureUrl, c.headers, '/api/storyboard/character-ref', buffer, mime);
export const uploadAudio = (c, buffer, mime) =>
studioUploadBinary(c.vivijureUrl, c.headers, '/api/storyboard/audio-upload', buffer, mime);
export const artifactUrl = (c, key) =>
`${trimTrailingSlashes(c.vivijureUrl)}/api/artifact/${key.split('/').map(encodeURIComponent).join('/')}`;
// --- projects -----------------------------------------------------------------
export const listProjects = (c) => studioGet(c.vivijureUrl, c.headers, '/api/storyboard/projects');
export const getProject = (c, id) =>
studioGet(c.vivijureUrl, c.headers, `/api/storyboard/projects/${encodeURIComponent(id)}`);
export const createProject = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/storyboard/projects', body);
export const updateProject = (c, id, body) =>
studioPatch(c.vivijureUrl, c.headers, `/api/storyboard/projects/${encodeURIComponent(id)}`, body);
export const saveProjectStoryboard = (c, id, storyboard) =>
studioPost(c.vivijureUrl, c.headers, `/api/storyboard/projects/${encodeURIComponent(id)}/storyboard`, { storyboard });
export const deleteProject = (c, id) =>
studioDelete(c.vivijureUrl, c.headers, `/api/storyboard/projects/${encodeURIComponent(id)}`);
// --- planning -----------------------------------------------------------------
export const planStoryboard = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/storyboard/plan', body);
export const refineStoryboard = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/storyboard/refine', body);
export const preflightStoryboard = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/storyboard/preflight', body);
export const enhanceStoryboard = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/storyboard/enhance', body);
export const bundleStoryboard = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/storyboard/bundle', body);
export const storyboardYaml = (c, storyboard) =>
studioPost(c.vivijureUrl, c.headers, '/api/storyboard/yaml', { storyboard });
export const storyboardMarkers = (c, body) =>
studioPost(c.vivijureUrl, c.headers, '/api/storyboard/markers', body);
export const studioChat = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/chat', body);
// --- score / audio ------------------------------------------------------------
export const startScoreBed = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/storyboard/score-bed', body);
export const pollJob = (c, jobId, moduleName) =>
studioGet(c.vivijureUrl, c.headers, `/api/job/${encodeURIComponent(jobId)}`, { module: moduleName });
export const analyzeAudio = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/audio/analyze', body);
// --- storyboard render bridge -------------------------------------------------
export const submitStoryboardRender = (c, body) =>
studioPost(c.vivijureUrl, c.headers, '/api/storyboard/render', body);
export const submitRenderPlan = (c, body) =>
studioPost(c.vivijureUrl, c.headers, '/api/storyboard/render-plan', body);
export const submitRenderFromKeyframes = (c, body) =>
studioPost(c.vivijureUrl, c.headers, '/api/storyboard/render-from-keyframes', body);
export const submitScatterRender = (c, body) =>
studioPost(c.vivijureUrl, c.headers, '/api/storyboard/render/scatter', body);
export const pollStoryboardRender = (c, jobId) =>
studioGet(c.vivijureUrl, c.headers, `/api/storyboard/render/${encodeURIComponent(jobId)}`);
export const cancelStoryboardRender = (c, jobId) =>
studioDelete(c.vivijureUrl, c.headers, `/api/storyboard/render/${encodeURIComponent(jobId)}`);
// --- film / clips orchestrator ------------------------------------------------
export const submitFilm = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/render/film', body);
export const pollFilm = (c, filmId) =>
studioGet(c.vivijureUrl, c.headers, `/api/render/film/${encodeURIComponent(filmId)}`);
export const submitClips = (c, body) => studioPost(c.vivijureUrl, c.headers, '/api/render/clips', body);
export const pollClips = (c, jobId) =>
studioGet(c.vivijureUrl, c.headers, `/api/render/clips/${encodeURIComponent(jobId)}`);
// --- render library -----------------------------------------------------------
export const listRenders = (c, query) => studioGet(c.vivijureUrl, c.headers, '/api/storyboard/renders', query);
export const listRenderTags = (c) => studioGet(c.vivijureUrl, c.headers, '/api/storyboard/renders/tags');
export const patchRender = (c, id, body) =>
studioPatch(c.vivijureUrl, c.headers, `/api/storyboard/renders/${encodeURIComponent(id)}`, body);
export const deleteRender = (c, id) =>
studioDelete(c.vivijureUrl, c.headers, `/api/storyboard/renders/${encodeURIComponent(id)}`);
export const regenShot = (c, id, body) =>
studioPost(c.vivijureUrl, c.headers, `/api/storyboard/renders/${encodeURIComponent(id)}/regen-shot`, body);
export const retryRender = (c, id, body = {}) =>
studioPost(c.vivijureUrl, c.headers, `/api/storyboard/renders/${encodeURIComponent(id)}/retry`, body);
export const addRenderAudio = (c, id, body) =>
studioPost(c.vivijureUrl, c.headers, `/api/storyboard/renders/${encodeURIComponent(id)}/add-audio`, body);
export const addRenderNarration = (c, id, body) =>
studioPost(c.vivijureUrl, c.headers, `/api/storyboard/renders/${encodeURIComponent(id)}/add-narration`, body);
export const finalizeRender = (c, id, body = {}) =>
studioPost(c.vivijureUrl, c.headers, `/api/storyboard/renders/${encodeURIComponent(id)}/finalize`, body);
export const animateRenderCloud = (c, id, body = {}) =>
studioPost(c.vivijureUrl, c.headers, `/api/storyboard/renders/${encodeURIComponent(id)}/animate-cloud`, body);
export const animateRenderHybrid = (c, id, body = {}) =>
studioPost(c.vivijureUrl, c.headers, `/api/storyboard/renders/${encodeURIComponent(id)}/animate-hybrid`, body);
export const adoptRender = (c, body) =>
studioPost(c.vivijureUrl, c.headers, '/api/storyboard/renders/adopt', body);