Reubencf commited on
Commit
fcf04cd
·
verified ·
1 Parent(s): 15f1bcd

Deploy Kiku AI

Browse files
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
- if(event.stage==='error')throw new Error('Generation failed');
 
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.':'The Modal generation could not finish. Try again; your project is safe.'});
 
 
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 a private Modal GPU through /api/modal/jobs. The browser
 
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 runs on our private Modal GPU through the authenticated
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
- if(engine===YUE2)return runModal<T>(name,input,{signal,status,progress,job});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  }