Files
OPEN_CODEX_API/src/services/audioGeneration.ts
T
2025-10-23 23:27:28 -03:00

361 lines
10 KiB
TypeScript

import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions';
import { apiService } from './api';
/**
* Tipos de vozes disponíveis para geração de áudio
*/
export type VoiceType = 'alloy' | 'echo' | 'fable' | 'nova' | 'onyx' | 'shimmer';
/**
* Interface para a resposta da API de geração de áudio
*/
export interface AudioGenerationResponse {
success: boolean;
audio_url: string; // URL do áudio gerado
audio_generation_id: string;
message: string; // Texto que foi convertido em áudio
}
/**
* Interface para um áudio armazenado no banco de dados
*/
export interface AudioRecord {
id: string;
user_email: string;
estabelecimento_id: number;
input_text: string;
model: string;
voice: VoiceType;
audio_url: string;
duration_seconds: number | null;
file_size: number;
cost_usd: string;
created_at: string;
}
/**
* Interface para os dados necessários para geração de áudio
*/
export interface AudioGenerationRequest {
message: string;
voice: VoiceType;
userEmail?: string;
estabelecimentoId?: number;
}
/**
* Informações sobre cada tipo de voz disponível
*/
export const VOICE_OPTIONS = {
alloy: {
label: "Alloy",
gender: "Masculina",
style: "Neutra, equilibrada, tom corporativo",
description: "Boa para tutoriais e comunicações institucionais."
},
echo: {
label: "Echo",
gender: "Masculina",
style: "Forte e profissional, mais grave",
description: "Ideal para voz de autoridade ou locução firme."
},
fable: {
label: "Fable",
gender: "Feminina",
style: "Narrativa, calorosa e envolvente",
description: "Ótima para storytelling e áudios empáticos."
},
onyx: {
label: "Onyx",
gender: "Masculina",
style: "Grave, autoritária, impactante",
description: "Excelente para trailers, mensagens sérias ou institucionais."
},
nova: {
label: "Nova",
gender: "Feminina",
style: "Brilhante, animada, energética",
description: "Boa para vídeos curtos, marketing ou conteúdos leves."
},
shimmer: {
label: "Shimmer",
gender: "Feminina",
style: "Suave, otimista, clara",
description: "Boa para mensagens acolhedoras, explicações e IA conversacional."
}
} as const;
/**
* Serviço de geração de áudio (Text-to-Speech)
*/
class AudioGenerationService {
private readonly AUDIO_GENERATION_ENDPOINT = '/webhook/codex/gerar_audio';
private readonly GET_AUDIOS_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/get_gerar_audios';
private readonly DELETE_AUDIO_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/delete_gerar_audio';
/**
* Gera um arquivo de áudio a partir de texto
*
* @param request - Dados da requisição (texto, voz, email, estabelecimento)
* @returns Promise com a resposta da API
*/
async generateAudio(request: AudioGenerationRequest): Promise<AudioGenerationResponse> {
const { message, voice, userEmail, estabelecimentoId } = request;
// Usa valores do .env se não forem fornecidos
const email = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);//userEmail || import.meta.env.VITE_USER_EMAIL || '';
const estabId = GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);//estabelecimentoId || parseInt(import.meta.env.VITE_ESTABELECIMENTO_ID) || 1;
// Valida o texto
if (!message || message.trim().length === 0) {
throw {
success: false,
message: 'O texto não pode estar vazio',
};
}
// Valida a voz
if (!this.isValidVoice(voice)) {
throw {
success: false,
message: `Voz inválida. Opções disponíveis: ${Object.keys(VOICE_OPTIONS).join(', ')}`,
};
}
// Log para debug (remover em produção se necessário)
console.log('Gerando áudio:', {
messageLength: message.length,
voice,
userEmail: email,
estabelecimentoId: estabId,
});
try {
// Faz a requisição usando o serviço de API
const response = await apiService.post<AudioGenerationResponse>(
this.AUDIO_GENERATION_ENDPOINT,
{
estabelecimento_id: estabId,
user_email: email,
message: message,
voice: voice,
}
);
return response.data;
} catch (error: any) {
// Trata erros específicos
console.error('Erro na geração de áudio:', error);
throw {
success: false,
message: error.message || 'Erro ao gerar áudio',
status: error.status,
};
}
}
/**
* Valida se a voz selecionada é suportada
*
* @param voice - Voz a ser validada
* @returns true se a voz é válida
*/
isValidVoice(voice: string): voice is VoiceType {
return Object.keys(VOICE_OPTIONS).includes(voice);
}
/**
* Obtém informações sobre uma voz específica
*
* @param voice - Tipo de voz
* @returns Informações da voz
*/
getVoiceInfo(voice: VoiceType) {
return VOICE_OPTIONS[voice];
}
/**
* Lista todas as vozes disponíveis
*
* @returns Array com todas as opções de voz
*/
getAllVoices() {
return Object.entries(VOICE_OPTIONS).map(([key, info]) => ({
value: key as VoiceType,
...info,
}));
}
/**
* Valida o texto para geração de áudio
*
* @param text - Texto a ser validado
* @param maxLength - Comprimento máximo (padrão: 4096 caracteres)
* @returns Objeto com resultado da validação
*/
validateText(text: string, maxLength: number = 4096): { valid: boolean; error?: string } {
if (!text || text.trim().length === 0) {
return {
valid: false,
error: 'O texto não pode estar vazio',
};
}
if (text.length > maxLength) {
return {
valid: false,
error: `O texto é muito longo. Máximo: ${maxLength} caracteres`,
};
}
return { valid: true };
}
/**
* Lista áudios do banco de dados com paginação
*
* @param userEmail - Email do usuário
* @param page - Número da página (padrão: 1)
* @param perPage - Quantidade de itens por página (padrão: 10)
* @returns Promise com o array de áudios
*/
async getAudios(
userEmail?: string,
page: number = 1,
perPage: number = 10
): Promise<AudioRecord[]> {
// Usa valores do GlobalFunctions se não forem fornecidos
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
}
console.log('Buscando áudios:', {
userEmail: email,
page,
perPage,
});
try {
// Faz a requisição GET com parâmetros na URL e query
const response = await apiService.get<AudioRecord[]>(
`${this.GET_AUDIOS_ENDPOINT}/${email}`,
{
params: {
page: page.toString(),
per_page: perPage.toString(),
},
}
);
console.log('Resposta completa da API:', response);
console.log('response.data:', response.data);
console.log('É array?:', Array.isArray(response.data));
// A API retorna diretamente o array de áudios
// Garante que sempre retorna um array
if (Array.isArray(response.data)) {
return response.data;
} else if (response.data && typeof response.data === 'object') {
// Se a resposta for um objeto com uma propriedade 'audios' ou similar
console.warn('API retornou objeto em vez de array:', response.data);
// Tenta encontrar o array dentro do objeto
if (Array.isArray((response.data as any).audios)) {
return (response.data as any).audios;
} else if (Array.isArray((response.data as any).data)) {
return (response.data as any).data;
}
}
// Se não conseguir extrair array, retorna vazio
console.warn('Não foi possível extrair array de áudios da resposta');
return [];
} catch (error: any) {
console.error('Erro ao buscar áudios:', error);
throw {
success: false,
message: error.message || 'Erro ao buscar áudios',
status: error.status,
};
}
}
/**
* Deleta um áudio do banco de dados
*
* @param audioId - ID do áudio a ser deletado
* @param userEmail - Email do usuário (opcional)
* @returns Promise com sucesso ou erro
*/
async deleteAudio(audioId: string, userEmail?: string): Promise<{ success: boolean; message?: string }> {
// Usa valores do GlobalFunctions se não forem fornecidos
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
}
if (!audioId) {
throw {
success: false,
message: 'ID do áudio não fornecido',
};
}
console.log('Deletando áudio:', {
audioId,
userEmail: email,
});
try {
// Faz a requisição DELETE com parâmetros na URL
const response = await apiService.delete<{ success: boolean; message?: string } | Array<{ success: boolean; message?: string }>>(
`${this.DELETE_AUDIO_ENDPOINT}/${email}/${audioId}`
);
console.log('Resposta completa do DELETE:', response);
console.log('response.data:', response.data);
// A API retorna um array com um objeto: [{"success":true}]
let result: { success: boolean; message?: string };
if (Array.isArray(response.data)) {
// Se for array, pega o primeiro elemento
result = response.data[0];
console.log('API retornou array, usando primeiro elemento:', result);
} else {
// Se for objeto direto
result = response.data;
console.log('API retornou objeto direto:', result);
}
// Garante que tem a estrutura mínima
return {
success: result.success ?? true,
message: result.message || 'Áudio deletado com sucesso',
};
} catch (error: any) {
console.error('Erro ao deletar áudio:', error);
throw {
success: false,
message: error.message || 'Erro ao deletar áudio',
status: error.status,
};
}
}
}
// Exporta instância única (Singleton)
export const audioGenerationService = new AudioGenerationService();