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
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:
parent
22d5ee4bd6
commit
078a4ea22b
3 changed files with 91 additions and 7 deletions
|
|
@ -652,10 +652,20 @@ class PacksApiV2 {
|
|||
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(
|
||||
voice,
|
||||
path: voicePath,
|
||||
url: presignedUrl ?? _voiceAbsoluteUrl(request, voice.id),
|
||||
url: presignedUrl,
|
||||
).toJson();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Future<CardPackBuyDto?> getPackBuy(String packId) async {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -87,12 +88,37 @@ class _CardVoiceControlsState extends State<CardVoiceControls> {
|
|||
throw Exception('Scope not available');
|
||||
}
|
||||
|
||||
// Flutter Web can't attach Authorization headers to the underlying
|
||||
// `<audio src=...>` request. Download bytes with Dio (Bearer token) and
|
||||
// play from memory instead.
|
||||
final bytes = await appScope.httpRepository.getVoiceFileBytes(
|
||||
requestedVoiceId,
|
||||
);
|
||||
// Use presigned URL directly if available (faster, no redirect needed)
|
||||
// Otherwise fallback to API endpoint which handles auth and redirects
|
||||
Uint8List bytes;
|
||||
if (voice.presignedUrl != null && voice.presignedUrl!.isNotEmpty) {
|
||||
// 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) {
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue