voices
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions

This commit is contained in:
Dmitry 2025-12-19 05:40:35 +03:00
parent 22d5ee4bd6
commit 078a4ea22b
3 changed files with 91 additions and 7 deletions

View file

@ -652,10 +652,20 @@ class PacksApiV2 {
objectId: voicePath, objectId: voicePath,
); );
if (presignedUrl == null) {
print('⚠️ Warning: Failed to generate presigned URL for voice ${voice.id} with objectId $voicePath');
// Fallback to API endpoint if presigned URL generation fails
return _voiceModelToDto(
voice,
path: voicePath,
url: _voiceAbsoluteUrl(request, voice.id),
).toJson();
}
return _voiceModelToDto( return _voiceModelToDto(
voice, voice,
path: voicePath, path: voicePath,
url: presignedUrl ?? _voiceAbsoluteUrl(request, voice.id), url: presignedUrl,
).toJson(); ).toJson();
} }

View file

@ -1010,6 +1010,54 @@ class HttpRepositoryV2 {
} }
} }
/// Download bytes from a URL (e.g., presigned URL from MinIO).
/// This method doesn't require authentication as presigned URLs are public.
/// Uses a separate Dio instance without interceptors to avoid adding auth headers.
Future<Uint8List> downloadBytesFromUrl(String url) async {
try {
// Create a separate Dio instance without interceptors for presigned URLs
final dio = Dio(
BaseOptions(
connectTimeout: ApiConfigV2.connectionTimeout,
receiveTimeout: ApiConfigV2.requestTimeout,
),
);
final response = await dio.get<List<int>>(
url,
options: Options(
responseType: ResponseType.bytes,
followRedirects: true,
validateStatus: (status) => status != null && status < 500,
),
);
if (response.statusCode != 200) {
throw ServerException(
message: 'Failed to download from URL: ${response.statusCode}',
statusCode: response.statusCode ?? 500,
);
}
final data = response.data;
if (data == null) {
throw const ServerException(
message: 'Empty response from URL',
statusCode: 500,
);
}
return Uint8List.fromList(data);
} on DioException catch (e) {
if (e.error is ApiException) {
rethrow;
}
throw NetworkException(
message: e.message ?? 'Network error',
originalError: e,
);
}
}
/// Get pack purchase details (includes ad reward offers) /// Get pack purchase details (includes ad reward offers)
Future<CardPackBuyDto?> getPackBuy(String packId) async { Future<CardPackBuyDto?> getPackBuy(String packId) async {
try { try {

View file

@ -1,4 +1,5 @@
import 'dart:developer'; import 'dart:developer';
import 'dart:typed_data';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -87,12 +88,37 @@ class _CardVoiceControlsState extends State<CardVoiceControls> {
throw Exception('Scope not available'); throw Exception('Scope not available');
} }
// Flutter Web can't attach Authorization headers to the underlying // Use presigned URL directly if available (faster, no redirect needed)
// `<audio src=...>` request. Download bytes with Dio (Bearer token) and // Otherwise fallback to API endpoint which handles auth and redirects
// play from memory instead. Uint8List bytes;
final bytes = await appScope.httpRepository.getVoiceFileBytes( if (voice.presignedUrl != null && voice.presignedUrl!.isNotEmpty) {
requestedVoiceId, // Download directly from presigned URL (no auth needed)
); bytes = await appScope.httpRepository.downloadBytesFromUrl(
voice.presignedUrl!,
);
} else if (voice.url != null && voice.url!.isNotEmpty) {
// Try to use URL from DTO (might be presigned URL or API endpoint)
// Check if it's a presigned URL (contains query params) or API endpoint
final uri = Uri.tryParse(voice.url!);
if (uri != null &&
(uri.scheme == 'http' || uri.scheme == 'https') &&
uri.queryParameters.isNotEmpty) {
// Likely a presigned URL, download directly
bytes = await appScope.httpRepository.downloadBytesFromUrl(
voice.url!,
);
} else {
// Fallback to API endpoint
bytes = await appScope.httpRepository.getVoiceFileBytes(
requestedVoiceId,
);
}
} else {
// Fallback to API endpoint
bytes = await appScope.httpRepository.getVoiceFileBytes(
requestedVoiceId,
);
}
if (!mounted || _currentVoiceId != requestedVoiceId) { if (!mounted || _currentVoiceId != requestedVoiceId) {
return; return;