Z.AI: Add video generation models

ad860f4447c77d8893a19481c789a2f7129bfaa3

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

3 files changed, +166 -11Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+64 -10
@@ -2297,7 +2297,7 @@ async function loadGoogleModels() {
2297}2297}
22982298
2299async function loadZaiModels() {2299async function loadZaiModels() {
2300 return ['cogview-4-250304'].map(name => ({ value: name, text: name }));2300 return ['cogview-4-250304', 'cogvideox-3', 'viduq1-text'].map(name => ({ value: name, text: name }));
2301}2301}
23022302
2303async function loadOpenRouterModels() {2303async function loadOpenRouterModels() {
@@ -3274,7 +3274,7 @@ async function generateExtrasImage(prompt, negativePrompt, signal) {
3274 * Gets an aspect ratio for Stability that is the closest to the given width and height.3274 * Gets an aspect ratio for Stability that is the closest to the given width and height.
3275 * @param {number} width Target width3275 * @param {number} width Target width
3276 * @param {number} height Target height3276 * @param {number} height Target height
3277 * @param {'google'|'stability'} source Source of the request, used to determine aspect ratio3277 * @param {'google'|'stability'|'zai'} source Source of the request, used to determine aspect ratio
3278 * @returns {string} Closest aspect ratio as a string3278 * @returns {string} Closest aspect ratio as a string
3279 */3279 */
3280function getClosestAspectRatio(width, height, source) {3280function getClosestAspectRatio(width, height, source) {
@@ -3300,6 +3300,12 @@ function getClosestAspectRatio(width, height, source) {
3300 '4:3': 4 / 3,3300 '4:3': 4 / 3,
3301 '3:4': 3 / 4,3301 '3:4': 3 / 4,
3302 };3302 };
3303 case 'zai':
3304 return {
3305 '1:1': 1,
3306 '16:9': 16 / 9,
3307 '9:16': 9 / 16,
3308 };
3303 default:3309 default:
3304 console.warn(`Unknown source "${source}" for aspect ratio calculation.`);3310 console.warn(`Unknown source "${source}" for aspect ratio calculation.`);
3305 return null;3311 return null;
@@ -3328,9 +3334,15 @@ function getClosestAspectRatio(width, height, source) {
3328 * Get closest size for Electron Hub3334 * Get closest size for Electron Hub
3329 * @param {number} width - The width of the image3335 * @param {number} width - The width of the image
3330 * @param {number} height - The height of the image3336 * @param {number} height - The height of the image
3337 * @param {string[]} sizes - Available sizes
3331 * @returns {Promise<string>} - The closest size3338 * @returns {Promise<string>} - The closest size
3332 */3339 */
3333async function getClosestSize(width, height) {3340async function getClosestSize(width, height, sizes = []) {
3341 const sizesData = [];
3342
3343 if (Array.isArray(sizes) && sizes.length > 0) {
3344 sizesData.push(...sizes);
3345 } else if (extension_settings.sd.source === sources.electronhub) {
3334 const response = await fetch('/api/sd/electronhub/sizes', {3346 const response = await fetch('/api/sd/electronhub/sizes', {
3335 method: 'POST',3347 method: 'POST',
3336 headers: getRequestHeaders(),3348 headers: getRequestHeaders(),
@@ -3343,7 +3355,20 @@ async function getClosestSize(width, height) {
3343 throw new Error(text);3355 throw new Error(text);
3344 }3356 }
3345 const result = await response.json();3357 const result = await response.json();
3346 const sizesData = result.sizes;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;
3368 }
3369
3370 const targetAspect = targetWidth / targetHeight;
3371 const targetResolution = targetWidth * targetHeight;
33473372
3348 const closestSize = sizesData.reduce((closest, size) => {3373 const closestSize = sizesData.reduce((closest, size) => {
3349 if (!size || typeof size !== 'string') {3374 if (!size || typeof size !== 'string') {
@@ -3356,16 +3381,14 @@ async function getClosestSize(width, height) {
33563381
3357 const sizeWidth = Number(sizeParts[0]);3382 const sizeWidth = Number(sizeParts[0]);
3358 const sizeHeight = Number(sizeParts[1]);3383 const sizeHeight = Number(sizeParts[1]);
3359 const targetWidth = Number(width);
3360 const targetHeight = Number(height);
33613384
3362 if (isNaN(sizeWidth) || isNaN(sizeHeight) || isNaN(targetWidth) || isNaN(targetHeight)) {3385 if (isNaN(sizeWidth) || isNaN(sizeHeight)) {
3363 return closest;3386 return closest;
3364 }3387 }
33653388
3366 const sizeArea = sizeWidth * sizeHeight;3389 const aspectDiff = Math.abs((sizeWidth / sizeHeight) - targetAspect) / targetAspect;
3367 const targetArea = targetWidth * targetHeight;3390 const resolutionDiff = Math.abs(sizeWidth * sizeHeight - targetResolution) / targetResolution;
3368 const diff = Math.abs(sizeArea - targetArea);3391 const diff = aspectDiff + resolutionDiff;
33693392
3370 return diff < closest.diff ? { size, diff } : closest;3393 return diff < closest.diff ? { size, diff } : closest;
3371 }, { size: null, diff: Infinity });3394 }, { size: null, diff: Infinity });
@@ -4245,6 +4268,36 @@ async function generateGoogleImage(prompt, negativePrompt, signal) {
4245 * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.4268 * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.
4246 */4269 */
4247async function generateZaiImage(prompt, signal) {4270async function generateZaiImage(prompt, signal) {
4271 if (/(cogvideox|vidu)/.test(extension_settings.sd.model)) {
4272 const videoParams = {};
4273 if (/cogvideox/.test(extension_settings.sd.model)) {
4274 const cogVideoSizes = ['1280x720', '720x1280', '1024x1024', '1080x1920', '2048x1080', '3840x2160'];
4275 videoParams.quality = extension_settings.sd.openai_quality === 'hd' ? 'quality' : 'speed';
4276 videoParams.size = await getClosestSize(extension_settings.sd.width, extension_settings.sd.height, cogVideoSizes);
4277 }
4278 if (/vidu/.test(extension_settings.sd.model)) {
4279 videoParams.aspect_ratio = getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'zai');
4280 }
4281
4282 const videoResult = await fetch('/api/sd/zai/generate-video', {
4283 method: 'POST',
4284 headers: getRequestHeaders(),
4285 signal: signal,
4286 body: JSON.stringify({
4287 prompt: prompt,
4288 model: extension_settings.sd.model,
4289 ...videoParams,
4290 }),
4291 });
4292
4293 if (videoResult.ok) {
4294 const data = await videoResult.json();
4295 return { format: data.format, data: data.video };
4296 }
4297
4298 const text = await videoResult.text();
4299 throw new Error(text);
4300 } else {
4248 // Round width and height to nearest multiple of 16, and clamp to 512-2048 range4301 // Round width and height to nearest multiple of 16, and clamp to 512-2048 range
4249 let width = clamp(Math.round(extension_settings.sd.width / 16) * 16, 512, 2048);4302 let width = clamp(Math.round(extension_settings.sd.width / 16) * 16, 512, 2048);
4250 let height = clamp(Math.round(extension_settings.sd.height / 16) * 16, 512, 2048);4303 let height = clamp(Math.round(extension_settings.sd.height / 16) * 16, 512, 2048);
@@ -4278,6 +4331,7 @@ async function generateZaiImage(prompt, signal) {
4278 const text = await result.text();4331 const text = await result.text();
4279 throw new Error(text);4332 throw new Error(text);
4280 }4333 }
4334}
42814335
4282/**4336/**
4283 * Generates an image using the OpenRouter API.4337 * Generates an image using the OpenRouter API.
public/scripts/extensions/stable-diffusion/settings.html+1 -1
@@ -187,7 +187,7 @@
187 <option value="high" data-i18n="High">High</option>187 <option value="high" data-i18n="High">High</option>
188 </select>188 </select>
189 </div>189 </div>
190 <div data-sd-model="dall-e-3,cogview-4" class="flex1">190 <div data-sd-model="dall-e-3,cogview-4,cogvideox" class="flex1">
191 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>191 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
192 <select id="sd_openai_quality">192 <select id="sd_openai_quality">
193 <option value="standard" data-i18n="Standard">Standard</option>193 <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) => {
1805 }1805 }
1806});1806});
18071807
1808zai.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
1808router.use('/comfy', comfy);1909router.use('/comfy', comfy);
1809router.use('/comfyrunpod', comfyRunPod);1910router.use('/comfyrunpod', comfyRunPod);
1810router.use('/together', together);1911router.use('/together', together);