Nova versao codex Parecer, atualizacao completa
This commit is contained in:
+92
-267
@@ -1,24 +1,15 @@
|
||||
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_url: string;
|
||||
audio_generation_id: string;
|
||||
message: string; // Texto que foi convertido em áudio
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface para um áudio armazenado no banco de dados
|
||||
*/
|
||||
export interface AudioRecord {
|
||||
id: string;
|
||||
user_email: string;
|
||||
@@ -33,9 +24,6 @@ export interface AudioRecord {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface para os dados necessários para geração de áudio
|
||||
*/
|
||||
export interface AudioGenerationRequest {
|
||||
message: string;
|
||||
voice: VoiceType;
|
||||
@@ -43,143 +31,80 @@ export interface AudioGenerationRequest {
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
}
|
||||
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)
|
||||
*/
|
||||
type ApiErrorShape = { message?: string; status?: number };
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
private resolveEmail(userEmail?: string): string {
|
||||
return userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
|
||||
}
|
||||
|
||||
private toApiError(error: unknown, fallback: string): never {
|
||||
const e = error as ApiErrorShape;
|
||||
throw { success: false, message: e?.message || fallback, status: e?.status };
|
||||
}
|
||||
|
||||
private extractArray<T>(data: unknown, keys: string[]): T[] {
|
||||
if (Array.isArray(data)) return data as T[];
|
||||
if (data && typeof data === 'object') {
|
||||
for (const key of keys) {
|
||||
const candidate = (data as Record<string, unknown>)[key];
|
||||
if (Array.isArray(candidate)) return candidate as T[];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -187,174 +112,74 @@ class AudioGenerationService {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`,
|
||||
};
|
||||
}
|
||||
|
||||
validateText(text: string, maxLength = 4096): { valid: boolean; error?: string } {
|
||||
if (!text?.trim()) 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);
|
||||
async generateAudio(request: AudioGenerationRequest): Promise<AudioGenerationResponse> {
|
||||
const { message, voice } = request;
|
||||
|
||||
if (!email) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Email do usuário não fornecido',
|
||||
};
|
||||
if (!message?.trim()) {
|
||||
throw { success: false, message: 'O texto não pode estar vazio' };
|
||||
}
|
||||
|
||||
console.log('Buscando áudios:', {
|
||||
userEmail: email,
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
if (!this.isValidVoice(voice)) {
|
||||
throw { success: false, message: `Voz inválida. Opções disponíveis: ${Object.keys(VOICE_OPTIONS).join(', ')}` };
|
||||
}
|
||||
|
||||
const email = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
|
||||
const estabId = GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);
|
||||
|
||||
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(),
|
||||
},
|
||||
}
|
||||
const response = await apiService.post<AudioGenerationResponse>(
|
||||
this.AUDIO_GENERATION_ENDPOINT,
|
||||
{ estabelecimento_id: estabId, user_email: email, message, voice }
|
||||
);
|
||||
|
||||
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,
|
||||
};
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
console.error('Erro na geração de áudio:', error);
|
||||
this.toApiError(error, 'Erro ao gerar áudio');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
async getAudios(userEmail?: string, page = 1, perPage = 10): Promise<AudioRecord[]> {
|
||||
const email = this.resolveEmail(userEmail);
|
||||
|
||||
if (!email) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Email do usuário não fornecido',
|
||||
};
|
||||
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.get<unknown>(
|
||||
`${this.GET_AUDIOS_ENDPOINT}/${email}`,
|
||||
{ params: { page: page.toString(), per_page: perPage.toString() } }
|
||||
);
|
||||
return this.extractArray<AudioRecord>(response.data, ['audios', 'data']);
|
||||
} catch (error: unknown) {
|
||||
console.error('Erro ao buscar áudios:', error);
|
||||
this.toApiError(error, 'Erro ao buscar áudios');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAudio(audioId: string, userEmail?: string): Promise<{ success: boolean; message?: string }> {
|
||||
const email = this.resolveEmail(userEmail);
|
||||
|
||||
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' };
|
||||
|
||||
try {
|
||||
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) {
|
||||
const result = Array.isArray(response.data) ? response.data[0] : response.data;
|
||||
return { success: result.success ?? true, message: result.message || 'Áudio deletado com sucesso' };
|
||||
} catch (error: unknown) {
|
||||
console.error('Erro ao deletar áudio:', error);
|
||||
|
||||
throw {
|
||||
success: false,
|
||||
message: error.message || 'Erro ao deletar áudio',
|
||||
status: error.status,
|
||||
};
|
||||
this.toApiError(error, 'Erro ao deletar áudio');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exporta instância única (Singleton)
|
||||
export const audioGenerationService = new AudioGenerationService();
|
||||
|
||||
Reference in New Issue
Block a user