Skip to content

Commit f9de5f6

Browse files
authored
Merge pull request #12 from deapi-ai/TEC-1574-audio-music
Tec 1574 audio music
2 parents fd02337 + 13f6696 commit f9de5f6

5 files changed

Lines changed: 215 additions & 12 deletions

File tree

src/components/EndpointForm.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -417,19 +417,35 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }:
417417
return undefined;
418418
}, [endpoint.id, endpoint.inferenceType, models, selectedModel, values]);
419419

420+
// Check if a field should be visible based on visibleWhen condition
421+
const isFieldVisible = useCallback((param: EndpointParam): boolean => {
422+
if (!param.visibleWhen) return true;
423+
const currentValue = values[param.visibleWhen.field];
424+
if (param.visibleWhen.matchEmpty && (currentValue === null || currentValue === undefined || currentValue === '')) {
425+
return true;
426+
}
427+
return param.visibleWhen.values.includes(currentValue as string);
428+
}, [values]);
429+
420430
const buildFilteredValues = useCallback((): Record<string, JsonValue> => {
421431
const filteredValues: Record<string, JsonValue> = {};
422432
Object.entries(values).forEach(([key, value]) => {
433+
// Exclude hidden fields from payload
434+
const param = endpoint.params.find(p => p.name === key);
435+
if (param?.visibleWhen && !isFieldVisible(param)) return;
436+
423437
if (value !== null) {
424438
// Split into array if arrayMode is on for this field
425439
if (arrayMode[key] && typeof value === 'string') {
426440
filteredValues[key] = value.split('\n').map(s => s.trim()).filter(Boolean);
441+
} else if (param?.valueType === 'number' && typeof value === 'string' && value !== '') {
442+
// Convert string select values to numbers when valueType requires it
443+
filteredValues[key] = Number(value);
427444
} else {
428445
filteredValues[key] = value;
429446
}
430447
} else {
431448
// For disabled nullable fields, send model default or param default
432-
const param = endpoint.params.find(p => p.name === key);
433449
// Try model default first, then param default
434450
if (modelDefaults) {
435451
const defaults = modelDefaults as Record<string, unknown>;
@@ -444,7 +460,7 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }:
444460
}
445461
});
446462
return filteredValues;
447-
}, [values, endpoint.params, modelDefaults, arrayMode]);
463+
}, [values, endpoint.params, modelDefaults, arrayMode, isFieldVisible]);
448464

449465
const handleCheckPrice = async () => {
450466
if (!endpoint.hasPriceCalc) return;
@@ -499,6 +515,8 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }:
499515

500516
Object.entries(files).forEach(([key, fileOrFiles]) => {
501517
const param = endpoint.params.find((p) => p.name === key);
518+
// Skip hidden file fields
519+
if (param?.visibleWhen && !isFieldVisible(param)) return;
502520
const isMultiMode = param?.multiFieldName && multiFileMode[key];
503521
const fieldName = isMultiMode && param?.multiFieldName ? param.multiFieldName : key;
504522

@@ -517,13 +535,14 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }:
517535
}
518536
};
519537

520-
// Categorize params for layout
538+
// Categorize params for layout (excluding hidden fields)
521539
const { promptParams, fileParams, selectParams, booleanParams, compactParams, otherParams } = useMemo(
522540
() => {
523541
const skip = isRequestStatusEndpoint ? ['request_id'] : [];
524-
return categorizeParams(endpoint.params, skip);
542+
const visibleParams = endpoint.params.filter((p) => isFieldVisible(p));
543+
return categorizeParams(visibleParams, skip);
525544
},
526-
[endpoint.params, isRequestStatusEndpoint]
545+
[endpoint.params, isRequestStatusEndpoint, isFieldVisible]
527546
);
528547

529548
return (

src/lib/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@ export const COMPACT_FORM_FIELDS = [
5454
'speed',
5555
'cfg_scale',
5656
'num_inference_steps',
57+
'mode',
5758
'lang',
5859
'voice',
5960
'format',
6061
'sample_rate',
62+
// txt2music
63+
'duration',
64+
'bpm',
65+
'inference_steps',
66+
'guidance_scale',
6167
];

src/lib/endpoint-registry.ts

Lines changed: 181 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export const ENDPOINT_GROUPS: EndpointGroupMeta[] = [
2222
icon: '🔊',
2323
description: 'Text-to-Speech',
2424
},
25+
{
26+
id: 'music-generation',
27+
label: 'Music Generation',
28+
icon: '🎵',
29+
description: 'Text-to-Music',
30+
},
2531
{
2632
id: 'transcription',
2733
label: 'Transcription',
@@ -309,7 +315,7 @@ export const ENDPOINTS: EndpointDefinition[] = [
309315
method: 'POST',
310316
path: '/txt2audio',
311317
description: 'Convert text to natural speech',
312-
contentType: 'json',
318+
contentType: 'multipart',
313319
isAsync: true,
314320
hasPriceCalc: true,
315321
priceCalcPath: '/txt2audio/price-calculation',
@@ -321,20 +327,62 @@ export const ENDPOINTS: EndpointDefinition[] = [
321327
required: true,
322328
placeholder: 'Text to convert to speech...',
323329
},
330+
{
331+
name: 'instruct',
332+
label: 'Instruct',
333+
type: 'textarea',
334+
required: false,
335+
placeholder: 'A young female voice, clear and warm',
336+
description: 'Voice description (voice_design) or style control (custom_voice)',
337+
visibleWhen: { field: 'mode', values: ['voice_design', 'custom_voice'], matchEmpty: true },
338+
},
339+
{
340+
name: 'ref_audio',
341+
label: 'Reference Audio',
342+
type: 'file',
343+
required: false,
344+
accept: 'audio/*,.mp3,.wav,.flac,.ogg,.m4a',
345+
description: 'Reference audio for voice cloning (mp3/wav/flac/ogg/m4a, max 10MB, 3-10s)',
346+
visibleWhen: { field: 'mode', values: ['voice_clone'] },
347+
},
348+
{
349+
name: 'ref_text',
350+
label: 'Reference Text',
351+
type: 'textarea',
352+
required: false,
353+
placeholder: 'Transcript of reference audio...',
354+
description: 'Transcript of ref audio for improved cloning accuracy',
355+
visibleWhen: { field: 'mode', values: ['voice_clone'] },
356+
},
324357
modelSelectParam(),
325358
{
326-
name: 'lang',
327-
label: 'Language',
359+
name: 'mode',
360+
label: 'Mode',
328361
type: 'select',
329-
required: true,
330-
description: 'Language for TTS',
362+
required: false,
363+
nullable: true,
364+
options: [
365+
{ value: 'custom_voice', label: 'Custom Voice' },
366+
{ value: 'voice_clone', label: 'Voice Clone' },
367+
{ value: 'voice_design', label: 'Voice Design' },
368+
],
369+
description: 'TTS mode. Null = model default.',
331370
},
332371
{
333372
name: 'voice',
334373
label: 'Voice',
335374
type: 'select',
375+
required: false,
376+
nullable: true,
377+
description: 'Voice to use (custom_voice mode)',
378+
visibleWhen: { field: 'mode', values: ['custom_voice'], matchEmpty: true },
379+
},
380+
{
381+
name: 'lang',
382+
label: 'Language',
383+
type: 'select',
336384
required: true,
337-
description: 'Voice to use for TTS',
385+
description: 'Language for TTS',
338386
},
339387
{
340388
name: 'format',
@@ -345,6 +393,7 @@ export const ENDPOINTS: EndpointDefinition[] = [
345393
options: [
346394
{ value: 'mp3', label: 'MP3' },
347395
{ value: 'wav', label: 'WAV' },
396+
{ value: 'flac', label: 'FLAC' },
348397
],
349398
},
350399
{
@@ -367,6 +416,132 @@ export const ENDPOINTS: EndpointDefinition[] = [
367416
],
368417
},
369418

419+
// ── Music Generation ────────────────────────────────────
420+
{
421+
id: 'txt2music',
422+
name: 'Text to Music',
423+
group: 'music-generation',
424+
method: 'POST',
425+
path: '/txt2music',
426+
description: 'Generate music from text description',
427+
contentType: 'json',
428+
isAsync: true,
429+
hasPriceCalc: true,
430+
priceCalcPath: '/txt2music/price-calculation',
431+
params: [
432+
{
433+
name: 'caption',
434+
label: 'Caption',
435+
type: 'textarea',
436+
required: true,
437+
placeholder: 'Describe the music (e.g. "upbeat pop song with electric guitar")',
438+
description: 'Music style and mood description',
439+
},
440+
{
441+
name: 'lyrics',
442+
label: 'Lyrics',
443+
type: 'textarea',
444+
required: false,
445+
nullable: true,
446+
default: '[Instrumental]',
447+
placeholder: '[verse]\nLyrics here...\n[chorus]\nChorus here...',
448+
description: 'Song lyrics or [Instrumental] for instrumental-only',
449+
},
450+
modelSelectParam(),
451+
{
452+
name: 'duration',
453+
label: 'Duration (s)',
454+
type: 'number',
455+
required: true,
456+
min: 10,
457+
max: 600,
458+
default: 30,
459+
description: 'Duration in seconds (10-600)',
460+
},
461+
{
462+
name: 'bpm',
463+
label: 'BPM',
464+
type: 'number',
465+
required: false,
466+
nullable: true,
467+
min: 30,
468+
max: 300,
469+
description: 'Beats per minute (empty for auto)',
470+
},
471+
{
472+
name: 'keyscale',
473+
label: 'Key / Scale',
474+
type: 'text',
475+
required: false,
476+
nullable: true,
477+
placeholder: 'C major',
478+
description: 'Musical key (e.g. "C major", "F# minor")',
479+
},
480+
{
481+
name: 'timesignature',
482+
label: 'Time Signature',
483+
type: 'select',
484+
required: false,
485+
nullable: true,
486+
valueType: 'number',
487+
options: [
488+
{ value: '2', label: '2/4' },
489+
{ value: '3', label: '3/4' },
490+
{ value: '4', label: '4/4' },
491+
{ value: '6', label: '6/8' },
492+
],
493+
description: 'Time signature (null for auto)',
494+
},
495+
{
496+
name: 'vocal_language',
497+
label: 'Vocal Language',
498+
type: 'text',
499+
required: false,
500+
nullable: true,
501+
default: 'en',
502+
placeholder: 'en',
503+
description: 'Language code for vocals',
504+
},
505+
{
506+
name: 'inference_steps',
507+
label: 'Steps',
508+
type: 'number',
509+
required: false,
510+
nullable: true,
511+
default: 8,
512+
min: 1,
513+
max: 100,
514+
description: '8 = turbo (fast), 32-100 = base (higher quality)',
515+
},
516+
{
517+
name: 'guidance_scale',
518+
label: 'Guidance Scale',
519+
type: 'number',
520+
required: false,
521+
nullable: true,
522+
default: 7.0,
523+
step: 0.5,
524+
min: 0,
525+
max: 20,
526+
description: 'How closely to follow the caption',
527+
},
528+
seedParam(),
529+
{
530+
name: 'format',
531+
label: 'Format',
532+
type: 'select',
533+
required: false,
534+
nullable: true,
535+
default: 'flac',
536+
options: [
537+
{ value: 'wav', label: 'WAV' },
538+
{ value: 'mp3', label: 'MP3' },
539+
{ value: 'flac', label: 'FLAC' },
540+
],
541+
},
542+
],
543+
},
544+
370545
// ── Transcription ─────────────────────────────────────────
371546
{
372547
id: 'vid2txt',

src/lib/format-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function getResultType(endpointId: string): 'image' | 'video' | 'audio' |
5959
if (endpointId.includes('video')) {
6060
return 'video';
6161
}
62-
if (endpointId.includes('audio') || endpointId.includes('txt2audio')) {
62+
if (endpointId.includes('audio') || endpointId.includes('txt2audio') || endpointId.includes('music')) {
6363
return 'audio';
6464
}
6565
return 'other';

src/lib/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface EndpointParam {
1919
multiFieldName?: string; // for file with multiple: alternative field name when in multi mode (e.g. "images")
2020
isPathParam?: boolean; // for params that go in URL path (e.g. /request-status/{request_id})
2121
supportsArray?: boolean; // allows toggling between single value and array of values (one per line)
22+
visibleWhen?: { field: string; values: string[]; matchEmpty?: boolean }; // conditional visibility based on another field's value
23+
valueType?: 'number'; // for select fields: convert string value to number in payload
2224
}
2325

2426
export interface EndpointDefinition {
@@ -40,6 +42,7 @@ export type EndpointGroup =
4042
| 'image-generation'
4143
| 'video-generation'
4244
| 'audio-generation'
45+
| 'music-generation'
4346
| 'transcription'
4447
| 'ocr'
4548
| 'image-utils'

0 commit comments

Comments
 (0)