Deploy Kiku AI
Browse files- app/api/modal/jobs/route.ts +8 -3
- lib/jobs.ts +2 -1
- lib/studio/engine.ts +30 -4
app/api/modal/jobs/route.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {modalEngine,modalFile} from '@/lib/modal-engine';
|
|
| 5 |
import {follow,jobOf,startJob} from '@/lib/modal-jobs';
|
| 6 |
export const runtime='nodejs';
|
| 7 |
export const maxDuration=1200;
|
|
|
|
|
|
|
| 8 |
|
| 9 |
/** Keep private Modal credentials on the server while streaming queue and GPU progress. The job outlives the connection; see modal-jobs. */
|
| 10 |
export async function POST(req:Request){
|
|
@@ -58,7 +60,8 @@ export async function POST(req:Request){
|
|
| 58 |
for await(const event of job){
|
| 59 |
signal.throwIfAborted();
|
| 60 |
if(event.type==='status'){
|
| 61 |
-
|
|
|
|
| 62 |
const step=event.progress_data?.find(p=>p.desc||p.progress!==null||p.length);
|
| 63 |
const fraction=step?.progress??(step?.index!=null&&step.length?(step.index+1)/step.length:null);
|
| 64 |
const running=(event as {original_msg?:string}).original_msg==='process_starts'||!!step||event.stage==='generating';
|
|
@@ -74,8 +77,10 @@ export async function POST(req:Request){
|
|
| 74 |
}
|
| 75 |
}
|
| 76 |
if(!received)throw new Error('No result');
|
| 77 |
-
}catch{
|
| 78 |
-
send({type:'error',error:signal.aborted?'The Modal generation was cancelled or timed out.'
|
|
|
|
|
|
|
| 79 |
}finally{signal.removeEventListener('abort',stop);client?.close();}
|
| 80 |
});
|
| 81 |
return follow(id,jobOf(id,auth.session.user.id)!,0);
|
|
|
|
| 5 |
import {follow,jobOf,startJob} from '@/lib/modal-jobs';
|
| 6 |
export const runtime='nodejs';
|
| 7 |
export const maxDuration=1200;
|
| 8 |
+
/** A failure the engine explained itself; its message is meant for the user. */
|
| 9 |
+
class EngineError extends Error{}
|
| 10 |
|
| 11 |
/** Keep private Modal credentials on the server while streaming queue and GPU progress. The job outlives the connection; see modal-jobs. */
|
| 12 |
export async function POST(req:Request){
|
|
|
|
| 60 |
for await(const event of job){
|
| 61 |
signal.throwIfAborted();
|
| 62 |
if(event.type==='status'){
|
| 63 |
+
// The engine's own message (a gr.Error) says what to change, such as a section with no clear beat.
|
| 64 |
+
if(event.stage==='error')throw new EngineError(typeof event.message==='string'?event.message.trim().slice(0,400):'');
|
| 65 |
const step=event.progress_data?.find(p=>p.desc||p.progress!==null||p.length);
|
| 66 |
const fraction=step?.progress??(step?.index!=null&&step.length?(step.index+1)/step.length:null);
|
| 67 |
const running=(event as {original_msg?:string}).original_msg==='process_starts'||!!step||event.stage==='generating';
|
|
|
|
| 77 |
}
|
| 78 |
}
|
| 79 |
if(!received)throw new Error('No result');
|
| 80 |
+
}catch(error){
|
| 81 |
+
send({type:'error',error:signal.aborted?'The Modal generation was cancelled or timed out.'
|
| 82 |
+
:error instanceof EngineError&&error.message?error.message
|
| 83 |
+
:'The Modal generation could not finish. Try again; your project is safe.'});
|
| 84 |
}finally{signal.removeEventListener('abort',stop);client?.close();}
|
| 85 |
});
|
| 86 |
return follow(id,jobOf(id,auth.session.user.id)!,0);
|
lib/jobs.ts
CHANGED
|
@@ -6,7 +6,8 @@ import type {CoverResult,Download,LyricsResult,PartResult,ScoreRead,ScoreResult,
|
|
| 6 |
* Every engine job the app runs: which Space serves it, how the request is
|
| 7 |
* checked and turned into the endpoint's arguments, and how the reply is read.
|
| 8 |
*
|
| 9 |
-
* YuE2-Turbo runs on
|
|
|
|
| 10 |
* calls the Hugging Face engines itself (see lib/studio/engine.ts).
|
| 11 |
* Hugging Face
|
| 12 |
* rate-limits Spaces by the caller's address, and this app's Docker Space shares
|
|
|
|
| 6 |
* Every engine job the app runs: which Space serves it, how the request is
|
| 7 |
* checked and turned into the endpoint's arguments, and how the reply is read.
|
| 8 |
*
|
| 9 |
+
* YuE2-Turbo runs on the Kiku-Engine ZeroGPU Space first, and on a private
|
| 10 |
+
* Modal GPU through /api/modal/jobs when ZeroGPU cannot serve it. The browser
|
| 11 |
* calls the Hugging Face engines itself (see lib/studio/engine.ts).
|
| 12 |
* Hugging Face
|
| 13 |
* rate-limits Spaces by the caller's address, and this app's Docker Space shares
|
lib/studio/engine.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
import type {Client} from '@gradio/client';
|
| 2 |
import type {Download} from '@/lib/types';
|
| 3 |
-
import {YUE2,JOBS,type Engine,type Input,type Job,asFile,explain,route} from '@/lib/jobs';
|
| 4 |
import {encodeWav} from './audio';
|
|
|
|
| 5 |
|
| 6 |
/** Raised when the session has lapsed, so the studio can show sign-in without losing the project. */
|
| 7 |
export class SignedOut extends Error{}
|
|
@@ -85,8 +86,8 @@ const backOnline=(signal:AbortSignal)=>new Promise<void>(done=>{
|
|
| 85 |
});
|
| 86 |
|
| 87 |
/**
|
| 88 |
-
* YuE2-Turbo
|
| 89 |
-
* app server. The job lives on the server, not in this connection: when
|
| 90 |
* Hugging Face's proxy drops the stream, or the browser goes offline, this
|
| 91 |
* reattaches and carries on from the last event it saw instead of losing the
|
| 92 |
* generation.
|
|
@@ -190,7 +191,31 @@ export async function runJob<T>(name:Job,input:Input,{signal,status,progress,job
|
|
| 190 |
if(seconds!==null&&seconds<limits.min-0.05)throw new Error(`Use at least ${limits.min} seconds of audio.`);
|
| 191 |
if(seconds!==null&&seconds>limits.max+0.05)throw new Error(`The engine takes up to ${limits.max/60} minutes at once. Select a shorter part; nothing was submitted.`);
|
| 192 |
}
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
const endpoint=typeof spec.endpoint==='function'?spec.endpoint(model):spec.endpoint;
|
| 195 |
const token=await hfToken();
|
| 196 |
const {handle_file}=await import('@gradio/client');
|
|
@@ -237,6 +262,7 @@ export async function runJob<T>(name:Job,input:Input,{signal,status,progress,job
|
|
| 237 |
if(signal.aborted||error instanceof SignedOut)throw error;
|
| 238 |
const message=error instanceof Error?error.message:'Generation could not finish.';
|
| 239 |
if(/\b401\b|unauthori[sz]ed|invalid.*token/i.test(message)){cached=null;throw new SignedOut('Sign in again to continue.');}
|
|
|
|
| 240 |
throw new Error(explain(message).slice(0,600));
|
| 241 |
}finally{client?.close();}
|
| 242 |
}
|
|
|
|
| 1 |
import type {Client} from '@gradio/client';
|
| 2 |
import type {Download} from '@/lib/types';
|
| 3 |
+
import {ENGINE,YUE2,JOBS,type Engine,type Input,type Job,type Spec,asFile,explain,route} from '@/lib/jobs';
|
| 4 |
import {encodeWav} from './audio';
|
| 5 |
+
import type {Model} from './models';
|
| 6 |
|
| 7 |
/** Raised when the session has lapsed, so the studio can show sign-in without losing the project. */
|
| 8 |
export class SignedOut extends Error{}
|
|
|
|
| 86 |
});
|
| 87 |
|
| 88 |
/**
|
| 89 |
+
* YuE2-Turbo falls back to our private Modal GPU, through the authenticated
|
| 90 |
+
* app server, when ZeroGPU cannot serve it. The job lives on the server, not in this connection: when
|
| 91 |
* Hugging Face's proxy drops the stream, or the browser goes offline, this
|
| 92 |
* reattaches and carries on from the last event it saw instead of losing the
|
| 93 |
* generation.
|
|
|
|
| 191 |
if(seconds!==null&&seconds<limits.min-0.05)throw new Error(`Use at least ${limits.min} seconds of audio.`);
|
| 192 |
if(seconds!==null&&seconds>limits.max+0.05)throw new Error(`The engine takes up to ${limits.max/60} minutes at once. Select a shorter part; nothing was submitted.`);
|
| 193 |
}
|
| 194 |
+
const call={signal,status,progress,job};
|
| 195 |
+
if(engine!==YUE2)return runSpace<T>(engine,spec,model,built,audio,call);
|
| 196 |
+
// YuE2-Turbo runs first on the Kiku-Engine ZeroGPU Space, on the user's own GPU
|
| 197 |
+
// allowance. When ZeroGPU cannot serve it (allowance used up, GPU time ran out,
|
| 198 |
+
// the Space down or overloaded), our private Modal GPU takes the job instead. A
|
| 199 |
+
// message the engine wrote about the request itself goes to the user as it is.
|
| 200 |
+
try{return await runSpace<T>(ENGINE,spec,model,built,audio,call,true);}
|
| 201 |
+
catch(error){
|
| 202 |
+
if(signal.aborted||!(error instanceof Unavailable))throw error;
|
| 203 |
+
status('The free GPU could not take this, so Kiku’s own GPU is taking over…');
|
| 204 |
+
return runModal<T>(name,input,call);
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/** ZeroGPU could not run the job; the message says why. */
|
| 209 |
+
class Unavailable extends Error{}
|
| 210 |
+
/**
|
| 211 |
+
* ZeroGPU and gateway failures, and the engine's catch-all for a crashed GPU
|
| 212 |
+
* worker (see failed() in the engine), as opposed to its messages about the request.
|
| 213 |
+
*/
|
| 214 |
+
const UNAVAILABLE=/quota|GPU.*duration|exceeded|allowance|gpu task aborted|timed out|timeout|no gpu|unavailable|starting|\b(429|50[0234])\b|too many requests|rate.?limit|<!doctype html|<html|fetch failed|failed to fetch|network|zerogpu|cuda|out of memory|too long to process at once|details are in the space logs|returned nothing|^generation failed\.?$|^error$/i;
|
| 215 |
+
|
| 216 |
+
/** Run a job on a Hugging Face Space with the user's own token. `fallback` raises Unavailable for failures another GPU could get past. */
|
| 217 |
+
async function runSpace<T>(engine:Engine,spec:Spec,model:Model,built:{args:(audio:unknown)=>unknown[]},audio:Blob|null,
|
| 218 |
+
{signal,status,progress}:Call,fallback=false):Promise<T>{
|
| 219 |
const endpoint=typeof spec.endpoint==='function'?spec.endpoint(model):spec.endpoint;
|
| 220 |
const token=await hfToken();
|
| 221 |
const {handle_file}=await import('@gradio/client');
|
|
|
|
| 262 |
if(signal.aborted||error instanceof SignedOut)throw error;
|
| 263 |
const message=error instanceof Error?error.message:'Generation could not finish.';
|
| 264 |
if(/\b401\b|unauthori[sz]ed|invalid.*token/i.test(message)){cached=null;throw new SignedOut('Sign in again to continue.');}
|
| 265 |
+
if(fallback&&UNAVAILABLE.test(message.trim()))throw new Unavailable(message);
|
| 266 |
throw new Error(explain(message).slice(0,600));
|
| 267 |
}finally{client?.close();}
|
| 268 |
}
|