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,
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue