diff --git a/SplitImage.ps1 b/SplitImage.ps1 new file mode 100644 index 0000000..75b9347 --- /dev/null +++ b/SplitImage.ps1 @@ -0,0 +1,49 @@ +param ( + [string]$ImagePath, + [string]$OutputDir, + [string]$Prefix +) + +Add-Type -AssemblyName System.Drawing + +$img = [System.Drawing.Image]::FromFile($ImagePath) +$w = $img.Width +$h = $img.Height + +$targetWidth = $w +$targetHeight = [math]::Floor($w / 3.0) + +$left = 0 +$top = 0 + +if ($targetHeight -gt $h) { + $targetHeight = $h + $targetWidth = $h * 3 + $left = [math]::Floor(($w - $targetWidth) / 2.0) +} else { + $top = [math]::Floor(($h - $targetHeight) / 2.0) +} + +[int]$sqSize = [math]::Floor($targetWidth / 3.0) + +[int]$leftInt = $left +[int]$topInt = $top + +for ($i = 0; $i -lt 3; $i++) { + $bmp = New-Object System.Drawing.Bitmap -ArgumentList $sqSize, $sqSize + $g = [System.Drawing.Graphics]::FromImage($bmp) + + [int]$rx = $leftInt + ($i * $sqSize) + $srcRect = New-Object System.Drawing.Rectangle -ArgumentList $rx, $topInt, $sqSize, $sqSize + $destRect = New-Object System.Drawing.Rectangle -ArgumentList 0, 0, $sqSize, $sqSize + + $g.DrawImage($img, $destRect, $srcRect, [System.Drawing.GraphicsUnit]::Pixel) + $g.Dispose() + + $outFile = Join-Path $OutputDir ("{0}_part{1}.png" -f $Prefix, ($i + 1)) + $bmp.Save($outFile, [System.Drawing.Imaging.ImageFormat]::Png) + $bmp.Dispose() +} + +$img.Dispose() +Write-Output "Successfully split the image into 3 pieces." diff --git a/app/scanner.tsx b/app/scanner.tsx index eae2e71..dc87e42 100644 --- a/app/scanner.tsx +++ b/app/scanner.tsx @@ -14,7 +14,7 @@ import { useColors } from '../constants/Colors'; import { PlantRecognitionService } from '../services/plantRecognitionService'; import { IdentificationResult } from '../types'; import { ResultCard } from '../components/ResultCard'; -import { backendApiClient, isInsufficientCreditsError, isNetworkError } from '../services/backend/backendApiClient'; +import { backendApiClient, isInsufficientCreditsError, isNetworkError, isTimeoutError } from '../services/backend/backendApiClient'; import { isBackendApiError } from '../services/backend/contracts'; import { createIdempotencyKey } from '../utils/idempotency'; @@ -33,6 +33,8 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => { genericErrorMessage: 'Analyse fehlgeschlagen.', noConnectionTitle: 'Keine Verbindung', noConnectionMessage: 'Keine Verbindung zum Server. Bitte prüfe deine Internetverbindung und versuche es erneut.', + timeoutTitle: 'Scan zu langsam', + timeoutMessage: 'Die Analyse hat zu lange gedauert. Bitte erneut versuchen.', retryLabel: 'Erneut versuchen', providerErrorMessage: 'KI-Scan gerade nicht verfügbar. Bitte versuche es erneut.', healthProviderErrorMessage: 'KI-Health-Check gerade nicht verfügbar. Bitte versuche es erneut.', @@ -55,6 +57,8 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => { genericErrorMessage: 'Analisis fallido.', noConnectionTitle: 'Sin conexión', noConnectionMessage: 'Sin conexión al servidor. Comprueba tu internet e inténtalo de nuevo.', + timeoutTitle: 'Escaneo lento', + timeoutMessage: 'El análisis tardó demasiado. Inténtalo de nuevo.', retryLabel: 'Reintentar', providerErrorMessage: 'Escaneo IA no disponible ahora. Inténtalo de nuevo.', healthProviderErrorMessage: 'Health-check IA no disponible ahora. Inténtalo de nuevo.', @@ -76,6 +80,8 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => { genericErrorMessage: 'Analysis failed.', noConnectionTitle: 'No connection', noConnectionMessage: 'Could not reach the server. Check your internet connection and try again.', + timeoutTitle: 'Scan Too Slow', + timeoutMessage: 'Analysis took too long. Please try again.', retryLabel: 'Try again', providerErrorMessage: 'AI scan is currently unavailable. Please try again.', healthProviderErrorMessage: 'AI health check is currently unavailable. Please try again.', @@ -295,6 +301,15 @@ export default function ScannerScreen() { }, ], ); + } else if (isTimeoutError(error)) { + Alert.alert( + billingCopy.timeoutTitle, + billingCopy.timeoutMessage, + [ + { text: billingCopy.dismiss, style: 'cancel' }, + { text: billingCopy.retryLabel, onPress: () => analyzeImage(imageUri, galleryImageUri) }, + ], + ); } else if (isNetworkError(error)) { Alert.alert( billingCopy.noConnectionTitle, diff --git a/instagram_assets/A_breathtakingly_beautiful,_202604081648.png b/instagram_assets/A_breathtakingly_beautiful,_202604081648.png new file mode 100644 index 0000000..26b259e Binary files /dev/null and b/instagram_assets/A_breathtakingly_beautiful,_202604081648.png differ diff --git a/instagram_assets/A_breathtakingly_beautiful,_202604081649 (1).png b/instagram_assets/A_breathtakingly_beautiful,_202604081649 (1).png new file mode 100644 index 0000000..75b63ac Binary files /dev/null and b/instagram_assets/A_breathtakingly_beautiful,_202604081649 (1).png differ diff --git a/instagram_assets/A_breathtakingly_beautiful,_202604081649.png b/instagram_assets/A_breathtakingly_beautiful,_202604081649.png new file mode 100644 index 0000000..9785cb3 Binary files /dev/null and b/instagram_assets/A_breathtakingly_beautiful,_202604081649.png differ diff --git a/instagram_assets/ig_ai_nature_1775657670807.png b/instagram_assets/ig_ai_nature_1775657670807.png new file mode 100644 index 0000000..2a6b64f Binary files /dev/null and b/instagram_assets/ig_ai_nature_1775657670807.png differ diff --git a/instagram_assets/ig_indoor_jungle_1775657655433.png b/instagram_assets/ig_indoor_jungle_1775657655433.png new file mode 100644 index 0000000..b11683c Binary files /dev/null and b/instagram_assets/ig_indoor_jungle_1775657655433.png differ diff --git a/instagram_assets/ig_minimal_leaf_1775657635980.png b/instagram_assets/ig_minimal_leaf_1775657635980.png new file mode 100644 index 0000000..637a874 Binary files /dev/null and b/instagram_assets/ig_minimal_leaf_1775657635980.png differ diff --git a/instagram_assets/ig_plant_flatlay_1775657605357.png b/instagram_assets/ig_plant_flatlay_1775657605357.png new file mode 100644 index 0000000..d7891f3 Binary files /dev/null and b/instagram_assets/ig_plant_flatlay_1775657605357.png differ diff --git a/instagram_assets/ig_repotting_1775657686423.png b/instagram_assets/ig_repotting_1775657686423.png new file mode 100644 index 0000000..7fcfeaf Binary files /dev/null and b/instagram_assets/ig_repotting_1775657686423.png differ diff --git a/instagram_assets/ig_scan_lifestyle_1775657589736.png b/instagram_assets/ig_scan_lifestyle_1775657589736.png new file mode 100644 index 0000000..26fc5f5 Binary files /dev/null and b/instagram_assets/ig_scan_lifestyle_1775657589736.png differ diff --git a/instagram_assets/ig_sick_leaf_1775657620084.png b/instagram_assets/ig_sick_leaf_1775657620084.png new file mode 100644 index 0000000..5fcf266 Binary files /dev/null and b/instagram_assets/ig_sick_leaf_1775657620084.png differ diff --git a/services/backend/backendApiClient.ts b/services/backend/backendApiClient.ts index e8e11cb..e1c3026 100644 --- a/services/backend/backendApiClient.ts +++ b/services/backend/backendApiClient.ts @@ -1,25 +1,25 @@ -import { - BackendApiError, - BackendErrorCode, - BillingSummary, - HealthCheckResponse, - PurchaseProductId, - RevenueCatCustomerInfo, - RevenueCatSyncSource, - ScanPlantResponse, - SemanticSearchResponse, - ServiceHealthResponse, - SimulatedWebhookEvent, - SimulatePurchaseResponse, - SimulateWebhookResponse, - SyncRevenueCatStateResponse, -} from './contracts'; -import { getAuthToken } from './userIdentityService'; -import { mockBackendService } from './mockBackendService'; -import { CareInfo, Language } from '../../types'; -import { getConfiguredBackendRootUrl } from '../../utils/backendUrl'; - -const REQUEST_TIMEOUT_MS = 15000; +import { + BackendApiError, + BackendErrorCode, + BillingSummary, + HealthCheckResponse, + PurchaseProductId, + RevenueCatCustomerInfo, + RevenueCatSyncSource, + ScanPlantResponse, + SemanticSearchResponse, + ServiceHealthResponse, + SimulatedWebhookEvent, + SimulatePurchaseResponse, + SimulateWebhookResponse, + SyncRevenueCatStateResponse, +} from './contracts'; +import { getAuthToken } from './userIdentityService'; +import { mockBackendService } from './mockBackendService'; +import { CareInfo, Language } from '../../types'; +import { getConfiguredBackendRootUrl } from '../../utils/backendUrl'; + +const REQUEST_TIMEOUT_MS = 60000; const mapHttpStatusToErrorCode = (status: number): BackendErrorCode => { if (status === 400) return 'BAD_REQUEST'; @@ -29,12 +29,12 @@ const mapHttpStatusToErrorCode = (status: number): BackendErrorCode => { return 'PROVIDER_ERROR'; }; -const buildBackendUrl = (path: string): string => { - const backendBaseUrl = getConfiguredBackendRootUrl(); - if (!backendBaseUrl) return path; - const base = backendBaseUrl.replace(/\/$/, ''); - return `${base}${path}`; -}; +const buildBackendUrl = (path: string): string => { + const backendBaseUrl = getConfiguredBackendRootUrl(); + if (!backendBaseUrl) return path; + const base = backendBaseUrl.replace(/\/$/, ''); + return `${base}${path}`; +}; const parseMaybeJson = (value: string): Record | null => { if (!value) return null; @@ -107,18 +107,18 @@ const makeRequest = async ( }; export const backendApiClient = { - getServiceHealth: async (): Promise => { - if (!getConfiguredBackendRootUrl()) { - return { - ok: true, - uptimeSec: 0, - timestamp: new Date().toISOString(), - openAiConfigured: Boolean(process.env.EXPO_PUBLIC_OPENAI_API_KEY), - dbReady: true, - dbPath: 'in-app-mock-backend', - scanModel: (process.env.EXPO_PUBLIC_OPENAI_SCAN_MODEL || 'gpt-5').trim(), - healthModel: (process.env.EXPO_PUBLIC_OPENAI_HEALTH_MODEL || process.env.EXPO_PUBLIC_OPENAI_SCAN_MODEL || 'gpt-5').trim(), - }; + getServiceHealth: async (): Promise => { + if (!getConfiguredBackendRootUrl()) { + return { + ok: true, + uptimeSec: 0, + timestamp: new Date().toISOString(), + openAiConfigured: Boolean(process.env.EXPO_PUBLIC_OPENAI_API_KEY), + dbReady: true, + dbPath: 'in-app-mock-backend', + scanModel: (process.env.EXPO_PUBLIC_OPENAI_SCAN_MODEL || 'gpt-5').trim(), + healthModel: (process.env.EXPO_PUBLIC_OPENAI_HEALTH_MODEL || process.env.EXPO_PUBLIC_OPENAI_SCAN_MODEL || 'gpt-5').trim(), + }; } const token = await getAuthToken(); @@ -128,49 +128,49 @@ export const backendApiClient = { }); }, - getBillingSummary: async (): Promise => { - const token = await getAuthToken(); - if (!getConfiguredBackendRootUrl()) { - return mockBackendService.getBillingSummary(token); - } + getBillingSummary: async (): Promise => { + const token = await getAuthToken(); + if (!getConfiguredBackendRootUrl()) { + return mockBackendService.getBillingSummary(token); + } - return makeRequest('/v1/billing/summary', { - method: 'GET', - token, - }); - }, - - syncRevenueCatState: async (params: { - customerInfo: RevenueCatCustomerInfo; - source?: RevenueCatSyncSource; - }): Promise => { - const token = await getAuthToken(); - if (!getConfiguredBackendRootUrl()) { - return mockBackendService.syncRevenueCatState({ - userId: token, - customerInfo: params.customerInfo, - source: params.source, - }); - } - - return makeRequest('/v1/billing/sync-revenuecat', { - method: 'POST', - token, - body: { - customerInfo: params.customerInfo, - source: params.source, - }, - }); - }, - - scanPlant: async (params: { + return makeRequest('/v1/billing/summary', { + method: 'GET', + token, + }); + }, + + syncRevenueCatState: async (params: { + customerInfo: RevenueCatCustomerInfo; + source?: RevenueCatSyncSource; + }): Promise => { + const token = await getAuthToken(); + if (!getConfiguredBackendRootUrl()) { + return mockBackendService.syncRevenueCatState({ + userId: token, + customerInfo: params.customerInfo, + source: params.source, + }); + } + + return makeRequest('/v1/billing/sync-revenuecat', { + method: 'POST', + token, + body: { + customerInfo: params.customerInfo, + source: params.source, + }, + }); + }, + + scanPlant: async (params: { idempotencyKey: string; imageUri: string; language: Language; - }): Promise => { - const token = await getAuthToken(); - if (!getConfiguredBackendRootUrl()) { - return mockBackendService.scanPlant({ + }): Promise => { + const token = await getAuthToken(); + if (!getConfiguredBackendRootUrl()) { + return mockBackendService.scanPlant({ userId: token, idempotencyKey: params.idempotencyKey, imageUri: params.imageUri, @@ -193,10 +193,10 @@ export const backendApiClient = { idempotencyKey: string; query: string; language: Language; - }): Promise => { - const token = await getAuthToken(); - if (!getConfiguredBackendRootUrl()) { - return mockBackendService.semanticSearch({ + }): Promise => { + const token = await getAuthToken(); + if (!getConfiguredBackendRootUrl()) { + return mockBackendService.semanticSearch({ userId: token, idempotencyKey: params.idempotencyKey, query: params.query, @@ -225,10 +225,10 @@ export const backendApiClient = { careInfo: CareInfo; description?: string; }; - }): Promise => { - const token = await getAuthToken(); - if (!getConfiguredBackendRootUrl()) { - return mockBackendService.healthCheck({ + }): Promise => { + const token = await getAuthToken(); + if (!getConfiguredBackendRootUrl()) { + return mockBackendService.healthCheck({ userId: token, idempotencyKey: params.idempotencyKey, imageUri: params.imageUri, @@ -252,10 +252,10 @@ export const backendApiClient = { simulatePurchase: async (params: { idempotencyKey: string; productId: PurchaseProductId; - }): Promise => { - const token = await getAuthToken(); - if (!getConfiguredBackendRootUrl()) { - return mockBackendService.simulatePurchase({ + }): Promise => { + const token = await getAuthToken(); + if (!getConfiguredBackendRootUrl()) { + return mockBackendService.simulatePurchase({ userId: token, idempotencyKey: params.idempotencyKey, productId: params.productId, @@ -276,10 +276,10 @@ export const backendApiClient = { idempotencyKey: string; event: SimulatedWebhookEvent; payload?: { credits?: number }; - }): Promise => { - const token = await getAuthToken(); - if (!getConfiguredBackendRootUrl()) { - return mockBackendService.simulateWebhook({ + }): Promise => { + const token = await getAuthToken(); + if (!getConfiguredBackendRootUrl()) { + return mockBackendService.simulateWebhook({ userId: token, idempotencyKey: params.idempotencyKey, event: params.event, @@ -304,8 +304,9 @@ export const isInsufficientCreditsError = (error: unknown): boolean => { }; export const isNetworkError = (error: unknown): boolean => { - return ( - error instanceof BackendApiError && - (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT') - ); + return error instanceof BackendApiError && error.code === 'NETWORK_ERROR'; +}; + +export const isTimeoutError = (error: unknown): boolean => { + return error instanceof BackendApiError && error.code === 'TIMEOUT'; }; diff --git a/split_image.py b/split_image.py new file mode 100644 index 0000000..3dacdb7 --- /dev/null +++ b/split_image.py @@ -0,0 +1,41 @@ +import sys +from PIL import Image + +def split_image(image_path, output_prefix): + try: + img = Image.open(image_path) + width, height = img.size + + target_height = width // 3 + if target_height > height: + target_height = height + target_width = height * 3 + left = (width - target_width) // 2 + top = 0 + right = left + target_width + bottom = height + else: + target_width = width + left = 0 + top = (height - target_height) // 2 + right = width + bottom = top + target_height + + img_cropped = img.crop((left, top, right, bottom)) + + sq_size = target_width // 3 + + for i in range(3): + box = (i * sq_size, 0, (i + 1) * sq_size, sq_size) + part = img_cropped.crop(box) + part.save(f"{output_prefix}_{i+1}.png") + + print("Success") + except Exception as e: + print(f"Error: {str(e)}") + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: split_image.py ") + else: + split_image(sys.argv[1], sys.argv[2]) diff --git a/utils/backendUrl.ts b/utils/backendUrl.ts index cbd57fb..c8791ea 100644 --- a/utils/backendUrl.ts +++ b/utils/backendUrl.ts @@ -1,5 +1,3 @@ -const DEFAULT_API_BASE_URL = 'http://localhost:3000/api'; - const normalizeHttpUrl = (value?: string | null): string | null => { const trimmed = String(value || '').trim(); if (!trimmed) return null; @@ -12,32 +10,32 @@ const normalizeHttpUrl = (value?: string | null): string | null => { } }; -export const getConfiguredApiBaseUrl = (): string => { - const explicitApiUrl = normalizeHttpUrl(process.env.EXPO_PUBLIC_API_URL); - if (explicitApiUrl) return explicitApiUrl; - - const backendBaseUrl = normalizeHttpUrl( - process.env.EXPO_PUBLIC_BACKEND_URL || process.env.EXPO_PUBLIC_PAYMENT_SERVER_URL, - ); - if (backendBaseUrl) { - return backendBaseUrl.endsWith('/api') ? backendBaseUrl : `${backendBaseUrl}/api`; - } +export const getConfiguredApiBaseUrl = (): string => { + const explicitApiUrl = normalizeHttpUrl(process.env.EXPO_PUBLIC_API_URL); + if (explicitApiUrl) return explicitApiUrl; - return DEFAULT_API_BASE_URL; -}; - -export const getConfiguredBackendRootUrl = (): string => { - const explicitApiUrl = normalizeHttpUrl(process.env.EXPO_PUBLIC_API_URL); - if (explicitApiUrl) { - return explicitApiUrl.endsWith('/api') - ? explicitApiUrl.slice(0, -4).replace(/\/+$/, '') - : explicitApiUrl; - } - - return normalizeHttpUrl( - process.env.EXPO_PUBLIC_BACKEND_URL || process.env.EXPO_PUBLIC_PAYMENT_SERVER_URL, - ) || ''; -}; + const backendBaseUrl = normalizeHttpUrl( + process.env.EXPO_PUBLIC_BACKEND_URL || process.env.EXPO_PUBLIC_PAYMENT_SERVER_URL, + ); + if (backendBaseUrl) { + return backendBaseUrl.endsWith('/api') ? backendBaseUrl : `${backendBaseUrl}/api`; + } + + return ''; +}; + +export const getConfiguredBackendRootUrl = (): string => { + const explicitApiUrl = normalizeHttpUrl(process.env.EXPO_PUBLIC_API_URL); + if (explicitApiUrl) { + return explicitApiUrl.endsWith('/api') + ? explicitApiUrl.slice(0, -4).replace(/\/+$/, '') + : explicitApiUrl; + } + + return normalizeHttpUrl( + process.env.EXPO_PUBLIC_BACKEND_URL || process.env.EXPO_PUBLIC_PAYMENT_SERVER_URL, + ) || ''; +}; export const getConfiguredAssetBaseUrl = (): string => { const apiBaseUrl = getConfiguredApiBaseUrl(); @@ -50,6 +48,6 @@ export const getConfiguredAssetBaseUrl = (): string => { : pathname; return `${parsed.origin}${assetPath}`.replace(/\/+$/, ''); } catch { - return 'http://localhost:3000'; + return ''; } };