From a1c340a7139ccc677c7127ebc04c0eb37ed4b242 Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 5 Dec 2025 17:26:09 +0100 Subject: [PATCH 1/3] fix: stt getModels, android model download location, vision following messages Signed-off-by: Jakub Mroz --- README.md | 23 +++++-- .../nitro/cactus/HybridCactusFileSystem.kt | 4 +- src/api/Database.ts | 67 +++++++++++++++++++ src/classes/CactusLM.ts | 6 -- src/classes/CactusSTT.ts | 15 ++--- src/hooks/useCactusSTT.ts | 4 +- src/index.tsx | 1 + src/native/Cactus.ts | 14 +++- src/types/CactusModel.ts | 1 + src/types/CactusSTTModel.ts | 10 +++ 10 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 src/types/CactusSTTModel.ts diff --git a/README.md b/README.md index 1b83d52..1d1c69f 100644 --- a/README.md +++ b/README.md @@ -577,7 +577,7 @@ Releases all resources associated with the model. Automatically calls `stop()` f **`getModels(): Promise`** -Fetches available models from the database and checks their download status. Results are cached in memory after the first call and subsequent calls return the cached results. +Fetches available models from the database and checks their download status. ### useCactusLM Hook @@ -603,7 +603,7 @@ The `useCactusLM` hook manages a `CactusLM` instance with reactive state. When m - `stop(): Promise` - Stops ongoing generation. Clears any errors. - `reset(): Promise` - Resets the model's internal state, clearing cached context. Also clears the `completion` state. - `destroy(): Promise` - Releases all resources associated with the model. Clears the `completion` state. Automatically called when the component unmounts. -- `getModels(): Promise` - Fetches available models from the database and checks their download status. Results are cached in memory and reused on subsequent calls. +- `getModels(): Promise` - Fetches available models from the database and checks their download status. ### CactusSTT Class @@ -662,9 +662,9 @@ Resets the model's internal state. Automatically calls `stop()` first. Releases all resources associated with the model. Automatically calls `stop()` first. Safe to call even if the model is not initialized. -**`getModels(): Promise`** +**`getModels(): Promise`** -Fetches available models from the database and checks their download status. Results are cached in memory after the first call and subsequent calls return the cached results. +Fetches available STT models from the database and checks their download status. ### useCactusSTT Hook @@ -689,7 +689,7 @@ The `useCactusSTT` hook manages a `CactusSTT` instance with reactive state. When - `stop(): Promise` - Stops ongoing generation. Clears any errors. - `reset(): Promise` - Resets the model's internal state. Also clears the `transcription` state. - `destroy(): Promise` - Releases all resources associated with the model. Clears the `transcription` state. Automatically called when the component unmounts. -- `getModels(): Promise` - Fetches available models from the database and checks their download status. Results are cached in memory and reused on subsequent calls. +- `getModels(): Promise` - Fetches available STT models from the database and checks their download status. ## Type Definitions @@ -826,6 +826,19 @@ interface CactusModel { downloadUrl: string; supportsToolCalling: boolean; supportsVision: boolean; + supportsCompletion: boolean; + createdAt: Date; + isDownloaded: boolean; +} +``` + +### CactusSTTModel + +```typescript +interface CactusSTTModel { + slug: string; + sizeMb: number; + downloadUrl: string; createdAt: Date; isDownloaded: boolean; } diff --git a/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt b/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt index dd4c394..3d7ae77 100644 --- a/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt +++ b/android/src/main/java/com/margelo/nitro/cactus/HybridCactusFileSystem.kt @@ -210,9 +210,7 @@ class HybridCactusFileSystem : HybridCactusFileSystemSpec() { } private fun cactusFile(): File { - val documentsDir = - context.getExternalFilesDir(android.os.Environment.DIRECTORY_DOCUMENTS) ?: context.filesDir - val cactusDir = File(documentsDir, "cactus") + val cactusDir = File(context.filesDir, "cactus") if (!cactusDir.exists()) { cactusDir.mkdirs() diff --git a/src/api/Database.ts b/src/api/Database.ts index e4ad636..5f3ce30 100644 --- a/src/api/Database.ts +++ b/src/api/Database.ts @@ -3,6 +3,7 @@ import type { DeviceInfo } from '../specs/CactusDeviceInfo.nitro'; import type { LogRecord } from '../telemetry/Telemetry'; import { packageVersion } from '../constants/packageVersion'; import type { CactusModel } from '../types/CactusModel'; +import type { CactusSTTModel } from '../types/CactusSTTModel'; interface CactusModelResponse { name: string; @@ -12,9 +13,18 @@ interface CactusModelResponse { download_url: string; supports_tool_calling: boolean; supports_vision: boolean; + supports_completion: boolean; created_at: Date; } +interface CactusSTTModelResponse { + slug: string; + download_url: string; + size_mb: number; + created_at: Date; + file_name: string; +} + export class Database { private static readonly url = 'https://vlqqczxwyaodtcdmdmlw.supabase.co'; private static readonly key = @@ -76,6 +86,38 @@ export class Database { downloadUrl: model.download_url, supportsToolCalling: model.supports_tool_calling, supportsVision: model.supports_vision, + supportsCompletion: model.supports_completion, + createdAt: model.created_at, + isDownloaded: false, + }; + } + + public static async getSTTModel(slug: string): Promise { + const response = await fetch( + `${this.url}/rest/v1/whisper?slug=eq.${slug}&select=*`, + { + headers: { + 'apikey': this.key, + 'Authorization': `Bearer ${this.key}`, + 'Accept-Profile': 'cactus', + }, + } + ); + + if (!response.ok) { + throw new Error('Getting STT model failed'); + } + + const [model] = (await response.json()) as CactusSTTModelResponse[]; + + if (!model) { + throw new Error(`STT model with slug "${slug}" not found`); + } + + return { + slug: model.slug, + downloadUrl: model.download_url, + sizeMb: model.size_mb, createdAt: model.created_at, isDownloaded: false, }; @@ -103,6 +145,31 @@ export class Database { downloadUrl: model.download_url, supportsToolCalling: model.supports_tool_calling, supportsVision: model.supports_vision, + supportsCompletion: model.supports_completion, + createdAt: model.created_at, + isDownloaded: false, + })); + } + + public static async getSTTModels(): Promise { + const response = await fetch(`${this.url}/rest/v1/whisper?select=*`, { + headers: { + 'apikey': this.key, + 'Authorization': `Bearer ${this.key}`, + 'Accept-Profile': 'cactus', + }, + }); + + if (!response.ok) { + throw new Error('Getting STT models failed'); + } + + const models = (await response.json()) as CactusSTTModelResponse[]; + + return models.map((model) => ({ + slug: model.slug, + downloadUrl: model.download_url, + sizeMb: model.size_mb, createdAt: model.created_at, isDownloaded: false, })); diff --git a/src/classes/CactusLM.ts b/src/classes/CactusLM.ts index f9defea..3b6f604 100644 --- a/src/classes/CactusLM.ts +++ b/src/classes/CactusLM.ts @@ -35,8 +35,6 @@ export class CactusLM { private static readonly defaultCompleteMode = 'local'; private static readonly defaultEmbedBufferSize = 2048; - private static cactusModelsCache: CactusModel[] | null = null; - constructor({ model, contextSize, corpusDir }: CactusLMParams = {}) { Telemetry.init(CactusConfig.telemetryToken); @@ -226,14 +224,10 @@ export class CactusLM { } public async getModels(): Promise { - if (CactusLM.cactusModelsCache) { - return CactusLM.cactusModelsCache; - } const models = await Database.getModels(); for (const model of models) { model.isDownloaded = await CactusFileSystem.modelExists(model.slug); } - CactusLM.cactusModelsCache = models; return models; } } diff --git a/src/classes/CactusSTT.ts b/src/classes/CactusSTT.ts index 243a4ab..ed672b2 100644 --- a/src/classes/CactusSTT.ts +++ b/src/classes/CactusSTT.ts @@ -7,11 +7,11 @@ import type { CactusSTTAudioEmbedParams, CactusSTTAudioEmbedResult, } from '../types/CactusSTT'; -import type { CactusModel } from '../types/CactusModel'; import { Telemetry } from '../telemetry/Telemetry'; import { CactusConfig } from '../config/CactusConfig'; import { Database } from '../api/Database'; import { getErrorMessage } from '../utils/error'; +import type { CactusSTTModel } from '../types/CactusSTTModel'; export class CactusSTT { private readonly cactus = new Cactus(); @@ -32,8 +32,6 @@ export class CactusSTT { }; private static readonly defaultEmbedBufferSize = 4096; - private static cactusModelsCache: CactusModel[] | null = null; - constructor({ model, contextSize }: CactusSTTParams = {}) { Telemetry.init(CactusConfig.telemetryToken); @@ -55,9 +53,10 @@ export class CactusSTT { this.isDownloading = true; try { + const model = await Database.getSTTModel(this.model); await CactusFileSystem.downloadModel( this.model, - `https://vlqqczxwyaodtcdmdmlw.supabase.co/storage/v1/object/public/voice-models/${this.model}.zip`, + model.downloadUrl, onProgress ); } finally { @@ -174,15 +173,11 @@ export class CactusSTT { this.isInitialized = false; } - public async getModels(): Promise { - if (CactusSTT.cactusModelsCache) { - return CactusSTT.cactusModelsCache; - } - const models = await Database.getModels(); + public async getModels(): Promise { + const models = await Database.getSTTModels(); for (const model of models) { model.isDownloaded = await CactusFileSystem.modelExists(model.slug); } - CactusSTT.cactusModelsCache = models; return models; } } diff --git a/src/hooks/useCactusSTT.ts b/src/hooks/useCactusSTT.ts index aadf19b..df7dd4b 100644 --- a/src/hooks/useCactusSTT.ts +++ b/src/hooks/useCactusSTT.ts @@ -10,7 +10,7 @@ import type { CactusSTTAudioEmbedParams, CactusSTTAudioEmbedResult, } from '../types/CactusSTT'; -import type { CactusModel } from '../types/CactusModel'; +import type { CactusSTTModel } from '../types/CactusSTTModel'; export const useCactusSTT = ({ model = 'whisper-small', @@ -254,7 +254,7 @@ export const useCactusSTT = ({ } }, [cactusSTT]); - const getModels = useCallback(async (): Promise => { + const getModels = useCallback(async (): Promise => { setError(null); try { return await cactusSTT.getModels(); diff --git a/src/index.tsx b/src/index.tsx index 565123e..fc28c4b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,6 +8,7 @@ export { useCactusSTT } from './hooks/useCactusSTT'; // Types export type { CactusModel } from './types/CactusModel'; +export type { CactusSTTModel } from './types/CactusSTTModel'; export type { CactusLMParams, CactusLMDownloadParams, diff --git a/src/native/Cactus.ts b/src/native/Cactus.ts index 00679b0..d2e3e10 100644 --- a/src/native/Cactus.ts +++ b/src/native/Cactus.ts @@ -32,11 +32,18 @@ export class Cactus { callback?: (token: string, tokenId: number) => void ): Promise { const messagesInternal: Message[] = []; - for (const message of messages) { - if (!message.images) { - messagesInternal.push(message); + for (let i = 0; i < messages.length; i++) { + const message = messages[i]!; + const isLastMessage = i === messages.length - 1; + + if (!message.images || !isLastMessage) { + messagesInternal.push({ + ...message, + images: undefined, + }); continue; } + const resizedImages: string[] = []; for (const imagePath of message.images) { const resizedImage = await CactusImage.resize( @@ -47,6 +54,7 @@ export class Cactus { ); resizedImages.push(resizedImage); } + messagesInternal.push({ ...message, images: resizedImages }); } diff --git a/src/types/CactusModel.ts b/src/types/CactusModel.ts index 9c37e19..38ba8dd 100644 --- a/src/types/CactusModel.ts +++ b/src/types/CactusModel.ts @@ -7,6 +7,7 @@ export interface CactusModel { downloadUrl: string; supportsToolCalling: boolean; supportsVision: boolean; + supportsCompletion: boolean; createdAt: Date; // Local diff --git a/src/types/CactusSTTModel.ts b/src/types/CactusSTTModel.ts new file mode 100644 index 0000000..6ac8ba3 --- /dev/null +++ b/src/types/CactusSTTModel.ts @@ -0,0 +1,10 @@ +export interface CactusSTTModel { + // API + slug: string; + sizeMb: number; + downloadUrl: string; + createdAt: Date; + + // Local + isDownloaded: boolean; +} From 5b6ae1f923e1ae4cc3919d19b8364decb73327db Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 5 Dec 2025 17:50:11 +0100 Subject: [PATCH 2/3] v1.2.1 Signed-off-by: Jakub Mroz --- example/ios/Podfile.lock | 4 ++-- package.json | 2 +- src/constants/packageVersion.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 0818af0..f900f1d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - Cactus (1.2.0): + - Cactus (1.2.1): - boost - DoubleConversion - fast_float @@ -2643,7 +2643,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - Cactus: 8853f351fa4c1ef40bad6d2b0152f503b713dc3e + Cactus: 9eae9838fd1c7be78375534369f7b07123ae645c DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: b8f1312d48447cca7b4abc21ed155db14742bd03 diff --git a/package.json b/package.json index 6d42725..ac11c61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cactus-react-native", - "version": "1.2.0", + "version": "1.2.1", "description": "Run AI models locally on mobile devices", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/src/constants/packageVersion.ts b/src/constants/packageVersion.ts index 9de35a5..9652abd 100644 --- a/src/constants/packageVersion.ts +++ b/src/constants/packageVersion.ts @@ -1 +1 @@ -export const packageVersion = '1.2.0'; +export const packageVersion = '1.2.1'; From 0b4c7eb45a8877b809bde0a206cc822fa3cf04db Mon Sep 17 00:00:00 2001 From: Jakub Mroz Date: Fri, 5 Dec 2025 18:03:33 +0100 Subject: [PATCH 3/3] fix: remove file_name from CactusModelResponse Signed-off-by: Jakub Mroz --- src/api/Database.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/Database.ts b/src/api/Database.ts index 5f3ce30..212f529 100644 --- a/src/api/Database.ts +++ b/src/api/Database.ts @@ -22,7 +22,6 @@ interface CactusSTTModelResponse { download_url: string; size_mb: number; created_at: Date; - file_name: string; } export class Database {