Z.AI: Add video generation models

ad860f4447c77d8893a19481c789a2f7129bfaa3

Cohee <18619528+Cohee1207@users.noreply.github.com>

3 files changed, +205 -50Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+103 -49
@@ -2297,7 +2297,7 @@ async function loadGoogleModels() {
22972297}
22982298
22992299async function loadZaiModels() {
23002300 return ['cogview-4-250304', 'cogvideox-3', 'viduq1-text'].map(name => ({ value: name, text: name }));
23012301}
23022302
23032303async function loadOpenRouterModels() {
@@ -3274,7 +3274,7 @@ async function generateExtrasImage(prompt, negativePrompt, signal) {
32743274 * Gets an aspect ratio for Stability that is the closest to the given width and height.
32753275 * @param {number} width Target width
32763276 * @param {number} height Target height
32773277 * @param {'google'|'stability'|'zai'} source Source of the request, used to determine aspect ratio
32783278 * @returns {string} Closest aspect ratio as a string
32793279 */
32803280function getClosestAspectRatio(width, height, source) {
@@ -3300,6 +3300,12 @@ function getClosestAspectRatio(width, height, source) {
33003300 '4:3': 4 / 3,
33013301 '3:4': 3 / 4,
33023302 };
3303+ case 'zai':
3304+ return {
3305+ '1:1': 1,
3306+ '16:9': 16 / 9,
3307+ '9:16': 9 / 16,
3308+ };
33033309 default:
33043310 console.warn(`Unknown source "${source}" for aspect ratio calculation.`);
33053311 return null;
@@ -3328,22 +3334,41 @@ function getClosestAspectRatio(width, height, source) {
33283334 * Get closest size for Electron Hub
33293335 * @param {number} width - The width of the image
33303336 * @param {number} height - The height of the image
3337+ * @param {string[]} sizes - Available sizes
33313338 * @returns {Promise<string>} - The closest size
33323339 */
33333340async function getClosestSize(width, height, sizes = []) {
3334- const response = await fetch('/api/sd/electronhub/sizes', {
3341+ const sizesData = [];
3335- method: 'POST',
3342+
3336- headers: getRequestHeaders(),
3343+ if (Array.isArray(sizes) && sizes.length > 0) {
3337- body: JSON.stringify({
3344+ sizesData.push(...sizes);
3338- model: extension_settings.sd.model,
3345+ } else if (extension_settings.sd.source === sources.electronhub) {
3339- }),
3346+ const response = await fetch('/api/sd/electronhub/sizes', {
3340- });
3347+ method: 'POST',
3341- if (!response.ok) {
3348+ headers: getRequestHeaders(),
3342- const text = await response.text();
3349+ body: JSON.stringify({
3343- throw new Error(text);
3350+ model: extension_settings.sd.model,
3351+ }),
3352+ });
3353+ if (!response.ok) {
3354+ const text = await response.text();
3355+ throw new Error(text);
3356+ }
3357+ const result = await response.json();
3358+ sizesData.push(...result.sizes);
3359+ } else {
3360+ return null;
3361+ }
3362+
3363+ const targetWidth = Number(width);
3364+ const targetHeight = Number(height);
3365+
3366+ if (isNaN(targetWidth) || isNaN(targetHeight)) {
3367+ return null;
33443368 }
3345- const result = await response.json();
3369+
33463370 const sizesDatatargetAspect = result.sizestargetWidth / targetHeight;
3371+ const targetResolution = targetWidth * targetHeight;
33473372
33483373 const closestSize = sizesData.reduce((closest, size) => {
33493374 if (!size || typeof size !== 'string') {
@@ -3356,16 +3381,14 @@ async function getClosestSize(width, height) {
33563381
33573382 const sizeWidth = Number(sizeParts[0]);
33583383 const sizeHeight = Number(sizeParts[1]);
3359- const targetWidth = Number(width);
3360- const targetHeight = Number(height);
33613384
33623385 if (isNaN(sizeWidth) || isNaN(sizeHeight) || isNaN(targetWidth) || isNaN(targetHeight)) {
33633386 return closest;
33643387 }
33653388
33663389 const sizeAreaaspectDiff = Math.abs((sizeWidth */ sizeHeight) - targetAspect) / targetAspect;
33673390 const targetArearesolutionDiff = targetWidthMath.abs(sizeWidth * targetHeightsizeHeight - targetResolution) / targetResolution;
33683391 const diff = Math.abs(sizeAreaaspectDiff -+ targetArea)resolutionDiff;
33693392
33703393 return diff < closest.diff ? { size, diff } : closest;
33713394 }, { size: null, diff: Infinity });
@@ -4245,38 +4268,69 @@ async function generateGoogleImage(prompt, negativePrompt, signal) {
42454268 * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.
42464269 */
42474270async function generateZaiImage(prompt, signal) {
4248- // Round width and height to nearest multiple of 16, and clamp to 512-2048 range
4271+ if (/(cogvideox|vidu)/.test(extension_settings.sd.model)) {
4249- let width = clamp(Math.round(extension_settings.sd.width / 16) * 16, 512, 2048);
4272+ const videoParams = {};
4250- let height = clamp(Math.round(extension_settings.sd.height / 16) * 16, 512, 2048);
4273+ if (/cogvideox/.test(extension_settings.sd.model)) {
4251-
4274+ const cogVideoSizes = ['1280x720', '720x1280', '1024x1024', '1080x1920', '2048x1080', '3840x2160'];
4252- // Make sure the pixel count does not exceed 2^21px
4275+ videoParams.quality = extension_settings.sd.openai_quality === 'hd' ? 'quality' : 'speed';
4253- while ((width * height) > Math.pow(2, 21)) {
4276+ videoParams.size = await getClosestSize(extension_settings.sd.width, extension_settings.sd.height, cogVideoSizes);
4254- if (width >= height) {
4277+ }
4255- width -= 16;
4278+ if (/vidu/.test(extension_settings.sd.model)) {
4256- } else {
4279+ videoParams.aspect_ratio = getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'zai');
4257- height -= 16;
42584280 }
4259- }
42604281
42614282 const resultvideoResult = await fetch('/api/sd/zai/generate-video', {
42624283 method: 'POST',
42634284 headers: getRequestHeaders(),
42644285 signal: signal,
42654286 body: JSON.stringify({
42664287 prompt: prompt,
42674288 model: extension_settings.sd.model,
4268- quality: extension_settings.sd.openai_quality,
4289+ ...videoParams,
4269- size: `${width}x${height}`,
4290+ }),
42704291 }),;
4271- });
42724292
42734293 if (resultvideoResult.ok) {
42744294 const data = await resultvideoResult.json();
42754295 return { format: data.format, data: data.imagevideo };
42764296 }
42774297
42784298 const text = await resultvideoResult.text();
42794299 throw new Error(text);
4300+ } else {
4301+ // Round width and height to nearest multiple of 16, and clamp to 512-2048 range
4302+ let width = clamp(Math.round(extension_settings.sd.width / 16) * 16, 512, 2048);
4303+ let height = clamp(Math.round(extension_settings.sd.height / 16) * 16, 512, 2048);
4304+
4305+ // Make sure the pixel count does not exceed 2^21px
4306+ while ((width * height) > Math.pow(2, 21)) {
4307+ if (width >= height) {
4308+ width -= 16;
4309+ } else {
4310+ height -= 16;
4311+ }
4312+ }
4313+
4314+ const result = await fetch('/api/sd/zai/generate', {
4315+ method: 'POST',
4316+ headers: getRequestHeaders(),
4317+ signal: signal,
4318+ body: JSON.stringify({
4319+ prompt: prompt,
4320+ model: extension_settings.sd.model,
4321+ quality: extension_settings.sd.openai_quality,
4322+ size: `${width}x${height}`,
4323+ }),
4324+ });
4325+
4326+ if (result.ok) {
4327+ const data = await result.json();
4328+ return { format: data.format, data: data.image };
4329+ }
4330+
4331+ const text = await result.text();
4332+ throw new Error(text);
4333+ }
42804334}
42814335
42824336/**
public/scripts/extensions/stable-diffusion/settings.html+1 -1
@@ -187,7 +187,7 @@
187187 <option value="high" data-i18n="High">High</option>
188188 </select>
189189 </div>
190190 <div data-sd-model="dall-e-3,cogview-4,cogvideox" class="flex1">
191191 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
192192 <select id="sd_openai_quality">
193193 <option value="standard" data-i18n="Standard">Standard</option>
src/endpoints/stable-diffusion.js+101 -0
@@ -1805,6 +1805,107 @@ zai.post('/generate', async (request, response) => {
18051805 }
18061806});
18071807
1808+zai.post('/generate-video', async (request, response) => {
1809+ try {
1810+ const controller = new AbortController();
1811+ request.socket.removeAllListeners('close');
1812+ request.socket.on('close', function () {
1813+ controller.abort();
1814+ });
1815+
1816+ const key = readSecret(request.user.directories, SECRET_KEYS.ZAI);
1817+
1818+ if (!key) {
1819+ console.warn('Z.AI key not found.');
1820+ return response.sendStatus(400);
1821+ }
1822+
1823+ console.debug('Z.AI video request:', request.body);
1824+
1825+ const generateResponse = await fetch('https://api.z.ai/api/paas/v4/videos/generations', {
1826+ method: 'POST',
1827+ headers: {
1828+ 'Content-Type': 'application/json',
1829+ 'Authorization': `Bearer ${key}`,
1830+ },
1831+ body: JSON.stringify({
1832+ prompt: request.body.prompt,
1833+ model: request.body.model,
1834+ quality: request.body.quality,
1835+ size: request.body.size,
1836+ aspect_ratio: request.body.aspect_ratio,
1837+ }),
1838+ signal: controller.signal,
1839+ });
1840+
1841+ if (!generateResponse.ok) {
1842+ const text = await generateResponse.text();
1843+ console.warn('Z.AI returned an error.', text);
1844+ return response.sendStatus(500);
1845+ }
1846+
1847+ /** @type {any} */
1848+ const data = await generateResponse.json();
1849+ console.debug('Z.AI video response:', data);
1850+
1851+ // Poll for video generation completion
1852+ for (let attempt = 0; attempt < 30; attempt++) {
1853+ if (controller.signal.aborted) {
1854+ console.info('Z.AI video generation aborted by client');
1855+ return response.status(500).send('Video generation aborted by client');
1856+ }
1857+
1858+ await delay(5000 + attempt * 1000);
1859+ console.debug(`Polling Z.AI video job ${data.id}, attempt ${attempt + 1}`);
1860+
1861+ const pollResponse = await fetch(`https://api.z.ai/api/paas/v4/async-result/${data.id}`, {
1862+ method: 'GET',
1863+ headers: {
1864+ 'Authorization': `Bearer ${key}`,
1865+ },
1866+ });
1867+
1868+ if (!pollResponse.ok) {
1869+ const text = await pollResponse.text();
1870+ console.warn('Z.AI video job polling failed', pollResponse.statusText, text);
1871+ return response.status(500).send(text);
1872+ }
1873+
1874+ /** @type {any} */
1875+ const pollResult = await pollResponse.json();
1876+ console.debug(`Z.AI video job status: ${pollResult.task_status}`);
1877+
1878+ if (pollResult.task_status === 'FAIL') {
1879+ console.warn('Z.AI video generation failed', pollResult);
1880+ return response.status(500).send('Video generation failed');
1881+ }
1882+
1883+ if (pollResult.task_status === 'SUCCESS') {
1884+ console.debug('Z.AI video generation succeeded', pollResult);
1885+ const url = pollResult?.video_result?.[0]?.url;
1886+
1887+ if (!url || !isValidUrl(url)) {
1888+ console.warn('Z.AI returned an invalid video URL.');
1889+ return response.sendStatus(500);
1890+ }
1891+
1892+ const contentResponse = await fetch(url);
1893+ if (!contentResponse.ok) {
1894+ const text = await contentResponse.text();
1895+ console.warn('Z.AI video content fetch failed', contentResponse.statusText, text);
1896+ return response.status(500).send(text);
1897+ }
1898+
1899+ const contentBuffer = await contentResponse.arrayBuffer();
1900+ return response.send({ format: 'mp4', video: Buffer.from(contentBuffer).toString('base64') });
1901+ }
1902+ }
1903+ } catch (error) {
1904+ console.error(error);
1905+ return response.sendStatus(500);
1906+ }
1907+});
1908+
18081909router.use('/comfy', comfy);
18091910router.use('/comfyrunpod', comfyRunPod);
18101911router.use('/together', together);