mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 10:43:38 +02:00
chore(packages): delete unused packages shared-gpu and nutriphi-database
Both have 0 consumers across the monorepo. Removing to reduce package count. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d3d11e661d
commit
9c1d16f580
19 changed files with 0 additions and 867 deletions
|
|
@ -1,5 +0,0 @@
|
|||
# Local development
|
||||
DATABASE_URL=postgresql://nutriphi:nutriphi_dev_password@localhost:5435/nutriphi
|
||||
|
||||
# Or use project-specific variable
|
||||
NUTRIPHI_DATABASE_URL=postgresql://nutriphi:nutriphi_dev_password@localhost:5435/nutriphi
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: nutriphi-postgres
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '5435:5432'
|
||||
environment:
|
||||
POSTGRES_DB: nutriphi
|
||||
POSTGRES_USER: nutriphi
|
||||
POSTGRES_PASSWORD: nutriphi_dev_password
|
||||
volumes:
|
||||
- nutriphi_postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U nutriphi -d nutriphi']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:latest
|
||||
container_name: nutriphi-pgadmin
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '5052:80'
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@nutriphi.local
|
||||
PGADMIN_DEFAULT_PASSWORD: admin
|
||||
volumes:
|
||||
- nutriphi_pgadmin_data:/var/lib/pgadmin
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
volumes:
|
||||
nutriphi_postgres_data:
|
||||
nutriphi_pgadmin_data:
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './dist/schema/index.js',
|
||||
out: './drizzle',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL || process.env.NUTRIPHI_DATABASE_URL || '',
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
{
|
||||
"name": "@manacore/nutriphi-database",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./schema": {
|
||||
"types": "./dist/schema/index.d.ts",
|
||||
"import": "./dist/schema/index.js",
|
||||
"require": "./dist/schema/index.js",
|
||||
"default": "./dist/schema/index.js"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./dist/client.d.ts",
|
||||
"import": "./dist/client.js",
|
||||
"require": "./dist/client.js",
|
||||
"default": "./dist/client.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"prepare": "pnpm build",
|
||||
"docker:up": "docker compose up -d",
|
||||
"docker:down": "docker compose down",
|
||||
"docker:logs": "docker compose logs -f postgres",
|
||||
"db:generate": "dotenv -- drizzle-kit generate",
|
||||
"db:migrate": "dotenv -- drizzle-kit migrate",
|
||||
"db:push": "dotenv -- drizzle-kit push --force",
|
||||
"db:studio": "dotenv -- drizzle-kit studio",
|
||||
"db:reset": "docker compose down -v && docker compose up -d && sleep 3 && pnpm db:push",
|
||||
"db:test": "dotenv -- tsx src/test-connection.ts",
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "^0.36.0",
|
||||
"postgres": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv-cli": "^7.4.0",
|
||||
"drizzle-kit": "^0.28.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.3",
|
||||
"@types/node": "^22.10.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from './schema/index.js';
|
||||
|
||||
// Singleton instance for the database client
|
||||
let dbInstance: ReturnType<typeof drizzle<typeof schema>> | null = null;
|
||||
let pgClient: ReturnType<typeof postgres> | null = null;
|
||||
|
||||
/**
|
||||
* Get the database URL from environment variables
|
||||
*/
|
||||
function getDatabaseUrl(): string {
|
||||
const url = process.env.DATABASE_URL || process.env.NUTRIPHI_DATABASE_URL;
|
||||
if (!url) {
|
||||
throw new Error(
|
||||
'Database URL not found. Set DATABASE_URL or NUTRIPHI_DATABASE_URL environment variable.'
|
||||
);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new database client
|
||||
* Uses connection pooling with sensible defaults for serverless environments
|
||||
*/
|
||||
export function createClient(connectionString?: string) {
|
||||
const url = connectionString || getDatabaseUrl();
|
||||
|
||||
const client = postgres(url, {
|
||||
max: 10, // Maximum connections in the pool
|
||||
idle_timeout: 20, // Close idle connections after 20 seconds
|
||||
connect_timeout: 10, // Connection timeout in seconds
|
||||
prepare: false, // Disable prepared statements for serverless
|
||||
});
|
||||
|
||||
return drizzle(client, { schema });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton database instance
|
||||
* Creates a new instance if one doesn't exist
|
||||
*/
|
||||
export function getDb() {
|
||||
if (!dbInstance) {
|
||||
const url = getDatabaseUrl();
|
||||
pgClient = postgres(url, {
|
||||
max: 10,
|
||||
idle_timeout: 20,
|
||||
connect_timeout: 10,
|
||||
prepare: false,
|
||||
});
|
||||
dbInstance = drizzle(pgClient, { schema });
|
||||
}
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
* Should be called when shutting down the application
|
||||
*/
|
||||
export async function closeDb() {
|
||||
if (pgClient) {
|
||||
await pgClient.end();
|
||||
pgClient = null;
|
||||
dbInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export the database type for typing purposes
|
||||
export type Database = ReturnType<typeof createClient>;
|
||||
|
||||
// Re-export commonly used Drizzle utilities
|
||||
export {
|
||||
eq,
|
||||
ne,
|
||||
gt,
|
||||
gte,
|
||||
lt,
|
||||
lte,
|
||||
and,
|
||||
or,
|
||||
not,
|
||||
inArray,
|
||||
notInArray,
|
||||
isNull,
|
||||
isNotNull,
|
||||
like,
|
||||
ilike,
|
||||
sql,
|
||||
asc,
|
||||
desc,
|
||||
count,
|
||||
sum,
|
||||
avg,
|
||||
min,
|
||||
max,
|
||||
} from 'drizzle-orm';
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// Database client exports
|
||||
export { createClient, getDb, closeDb, type Database } from './client.js';
|
||||
|
||||
// Re-export Drizzle utilities
|
||||
export {
|
||||
eq,
|
||||
ne,
|
||||
gt,
|
||||
gte,
|
||||
lt,
|
||||
lte,
|
||||
and,
|
||||
or,
|
||||
not,
|
||||
inArray,
|
||||
notInArray,
|
||||
isNull,
|
||||
isNotNull,
|
||||
like,
|
||||
ilike,
|
||||
sql,
|
||||
asc,
|
||||
desc,
|
||||
count,
|
||||
sum,
|
||||
avg,
|
||||
min,
|
||||
max,
|
||||
} from './client.js';
|
||||
|
||||
// Schema exports
|
||||
export * from './schema/index.js';
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { pgTable, uuid, text, integer, timestamp } from 'drizzle-orm/pg-core';
|
||||
|
||||
/**
|
||||
* Nutrition goals table - stores user's daily nutrition targets
|
||||
*/
|
||||
export const nutritionGoals = pgTable('nutrition_goals', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id').notNull().unique(),
|
||||
caloriesTarget: integer('calories_target').notNull(),
|
||||
proteinTarget: integer('protein_target').notNull(),
|
||||
carbsTarget: integer('carbs_target').notNull(),
|
||||
fatTarget: integer('fat_target').notNull(),
|
||||
fiberTarget: integer('fiber_target'),
|
||||
sugarLimit: integer('sugar_limit'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Type exports
|
||||
export type NutritionGoal = typeof nutritionGoals.$inferSelect;
|
||||
export type NewNutritionGoal = typeof nutritionGoals.$inferInsert;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// Meal schema and types
|
||||
export { meals, type Meal, type NewMeal, type FoodItem } from './meals.js';
|
||||
|
||||
// Goals schema and types
|
||||
export { nutritionGoals, type NutritionGoal, type NewNutritionGoal } from './goals.js';
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { pgTable, uuid, text, integer, real, timestamp, index, jsonb } from 'drizzle-orm/pg-core';
|
||||
|
||||
/**
|
||||
* Meals table - stores all meal entries with nutrition data
|
||||
*/
|
||||
export const meals = pgTable(
|
||||
'meals',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id').notNull(),
|
||||
foodName: text('food_name').notNull(),
|
||||
imageUrl: text('image_url'),
|
||||
storagePath: text('storage_path'), // R2 path for deletion
|
||||
calories: real('calories').default(0),
|
||||
protein: real('protein').default(0),
|
||||
carbohydrates: real('carbohydrates').default(0),
|
||||
fat: real('fat').default(0),
|
||||
fiber: real('fiber').default(0),
|
||||
sugar: real('sugar').default(0),
|
||||
sodium: real('sodium').default(0),
|
||||
servingSize: text('serving_size'),
|
||||
mealType: text('meal_type'), // breakfast | lunch | dinner | snack
|
||||
analysisStatus: text('analysis_status').default('pending'), // pending | completed | failed | manual
|
||||
healthScore: integer('health_score'), // 1-10
|
||||
healthCategory: text('health_category'), // very_healthy | healthy | moderate | unhealthy
|
||||
notes: text('notes'),
|
||||
userRating: integer('user_rating'), // 1-5
|
||||
foodItems: jsonb('food_items').$type<FoodItem[]>().default([]),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('meals_user_id_idx').on(table.userId),
|
||||
index('meals_created_at_idx').on(table.createdAt),
|
||||
index('meals_user_created_idx').on(table.userId, table.createdAt),
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Food item type for meal ingredients
|
||||
*/
|
||||
export interface FoodItem {
|
||||
id: string;
|
||||
name: string;
|
||||
category:
|
||||
| 'protein'
|
||||
| 'vegetable'
|
||||
| 'grain'
|
||||
| 'fruit'
|
||||
| 'dairy'
|
||||
| 'fat'
|
||||
| 'processed'
|
||||
| 'beverage';
|
||||
portionSize: string;
|
||||
calories?: number;
|
||||
protein?: number;
|
||||
carbs?: number;
|
||||
fat?: number;
|
||||
fiber?: number;
|
||||
sugar?: number;
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
// Type exports
|
||||
export type Meal = typeof meals.$inferSelect;
|
||||
export type NewMeal = typeof meals.$inferInsert;
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"name": "@manacore/shared-gpu",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Client library for Mana GPU services (STT, TTS, Image Generation)",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import type { GpuServiceConfig } from './types';
|
||||
import { SttClient } from './stt-client';
|
||||
import { TtsClient } from './tts-client';
|
||||
import { ImageClient } from './image-client';
|
||||
|
||||
/**
|
||||
* Unified client for all Mana GPU services.
|
||||
*
|
||||
* @example Public URLs (from anywhere):
|
||||
* ```ts
|
||||
* const gpu = new GpuClient({ baseUrl: 'https://gpu.mana.how' });
|
||||
* ```
|
||||
*
|
||||
* @example LAN (direct):
|
||||
* ```ts
|
||||
* const gpu = new GpuClient({ baseUrl: 'http://192.168.178.11' });
|
||||
* ```
|
||||
*
|
||||
* @example Custom URLs:
|
||||
* ```ts
|
||||
* const gpu = new GpuClient({
|
||||
* baseUrl: '',
|
||||
* urls: { stt: 'https://gpu-stt.mana.how', tts: 'https://gpu-tts.mana.how' },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class GpuClient {
|
||||
public readonly stt: SttClient;
|
||||
public readonly tts: TtsClient;
|
||||
public readonly image: ImageClient;
|
||||
public readonly apiKey?: string;
|
||||
|
||||
constructor(config: GpuServiceConfig) {
|
||||
this.apiKey = config.apiKey;
|
||||
this.stt = new SttClient(config);
|
||||
this.tts = new TtsClient(config);
|
||||
this.image = new ImageClient(config);
|
||||
}
|
||||
|
||||
/** Check health of all GPU services. */
|
||||
async healthCheck(): Promise<{
|
||||
stt: boolean;
|
||||
tts: boolean;
|
||||
image: boolean;
|
||||
}> {
|
||||
const [sttHealth, ttsHealth, imageHealth] = await Promise.allSettled([
|
||||
this.stt.health(),
|
||||
this.tts.health(),
|
||||
this.image.health(),
|
||||
]);
|
||||
|
||||
return {
|
||||
stt: sttHealth.status === 'fulfilled' && sttHealth.value.status === 'healthy',
|
||||
tts: ttsHealth.status === 'fulfilled' && ttsHealth.value.status === 'healthy',
|
||||
image: imageHealth.status === 'fulfilled' && imageHealth.value.status === 'healthy',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import type {
|
||||
GenerateImageOptions,
|
||||
GenerateImageResult,
|
||||
ImageGenHealthResponse,
|
||||
GpuServiceConfig,
|
||||
} from './types';
|
||||
import { resolveServiceUrl } from './resolve-url';
|
||||
|
||||
export class ImageClient {
|
||||
private baseUrl: string;
|
||||
private timeout: number;
|
||||
private apiKey?: string;
|
||||
|
||||
constructor(config: GpuServiceConfig) {
|
||||
this.baseUrl = resolveServiceUrl(config, 'image');
|
||||
this.timeout = config.timeout ?? 120_000;
|
||||
this.apiKey = config.apiKey;
|
||||
}
|
||||
|
||||
/** Generate an image from a text prompt. */
|
||||
async generate(options: GenerateImageOptions): Promise<GenerateImageResult> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.apiKey ? { 'X-API-Key': this.apiKey } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: options.prompt,
|
||||
width: options.width ?? 1024,
|
||||
height: options.height ?? 1024,
|
||||
steps: options.steps ?? 4,
|
||||
seed: options.seed,
|
||||
output_format: options.outputFormat ?? 'png',
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(
|
||||
`Image generation error ${response.status}: ${(error as { detail: string }).detail}`
|
||||
);
|
||||
}
|
||||
|
||||
return (await response.json()) as GenerateImageResult;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the full URL for a generated image. */
|
||||
imageUrl(relativePath: string): string {
|
||||
return `${this.baseUrl}${relativePath}`;
|
||||
}
|
||||
|
||||
/** Download a generated image as ArrayBuffer. */
|
||||
async downloadImage(relativePath: string): Promise<ArrayBuffer> {
|
||||
const response = await fetch(this.imageUrl(relativePath), {
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Failed to download image: ${response.status}`);
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
/** Check if the image generation service is healthy. */
|
||||
async health(): Promise<ImageGenHealthResponse> {
|
||||
const response = await fetch(`${this.baseUrl}/health`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return (await response.json()) as ImageGenHealthResponse;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
export { GpuClient } from './gpu-client';
|
||||
export { SttClient } from './stt-client';
|
||||
export { TtsClient } from './tts-client';
|
||||
export { ImageClient } from './image-client';
|
||||
export { resolveServiceUrl } from './resolve-url';
|
||||
export { GPU_PUBLIC_URLS, GPU_LAN_URLS } from './types';
|
||||
export type {
|
||||
// Config
|
||||
GpuServiceConfig,
|
||||
// STT
|
||||
TranscriptionResult,
|
||||
TranscribeOptions,
|
||||
WordTimestamp,
|
||||
Segment,
|
||||
// TTS
|
||||
SynthesizeOptions,
|
||||
TTSVoice,
|
||||
TTSVoiceType,
|
||||
TTSHealthResponse,
|
||||
// Image
|
||||
GenerateImageOptions,
|
||||
GenerateImageResult,
|
||||
ImageGenHealthResponse,
|
||||
} from './types';
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import type { GpuServiceConfig } from './types';
|
||||
import { GPU_PUBLIC_URLS } from './types';
|
||||
|
||||
type ServiceKey = 'llm' | 'stt' | 'tts' | 'image' | 'ollama';
|
||||
|
||||
const LAN_PORTS: Record<ServiceKey, number> = {
|
||||
llm: 3025,
|
||||
stt: 3020,
|
||||
tts: 3022,
|
||||
image: 3023,
|
||||
ollama: 11434,
|
||||
};
|
||||
|
||||
/** Resolve the URL for a specific GPU service based on config. */
|
||||
export function resolveServiceUrl(config: GpuServiceConfig, service: ServiceKey): string {
|
||||
// 1. Explicit override
|
||||
if (config.urls?.[service]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return config.urls[service]!;
|
||||
}
|
||||
|
||||
const base = config.baseUrl;
|
||||
|
||||
// 2. Public mode: "https://gpu.mana.how" → "https://gpu-stt.mana.how"
|
||||
if (base.includes('gpu.mana.how')) {
|
||||
return GPU_PUBLIC_URLS[service];
|
||||
}
|
||||
|
||||
// 3. LAN mode: "http://192.168.178.11" → "http://192.168.178.11:3020"
|
||||
return `${base.replace(/\/$/, '')}:${LAN_PORTS[service]}`;
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import type { TranscriptionResult, TranscribeOptions, GpuServiceConfig } from './types';
|
||||
import { resolveServiceUrl } from './resolve-url';
|
||||
|
||||
export class SttClient {
|
||||
private baseUrl: string;
|
||||
private timeout: number;
|
||||
private apiKey?: string;
|
||||
|
||||
constructor(config: GpuServiceConfig) {
|
||||
this.baseUrl = resolveServiceUrl(config, 'stt');
|
||||
this.timeout = config.timeout ?? 60_000;
|
||||
this.apiKey = config.apiKey;
|
||||
}
|
||||
|
||||
/** Transcribe audio with optional word timestamps and speaker diarization. */
|
||||
async transcribe(
|
||||
audioBuffer: Buffer | Blob,
|
||||
filename: string,
|
||||
options: TranscribeOptions = {}
|
||||
): Promise<TranscriptionResult> {
|
||||
const formData = new FormData();
|
||||
const blob =
|
||||
audioBuffer instanceof Blob ? audioBuffer : new Blob([new Uint8Array(audioBuffer)]);
|
||||
formData.append('file', blob, filename);
|
||||
|
||||
if (options.language) formData.append('language', options.language);
|
||||
if (options.model) formData.append('model', options.model);
|
||||
formData.append('align', String(options.align ?? true));
|
||||
formData.append('diarize', String(options.diarize ?? false));
|
||||
if (options.minSpeakers != null) formData.append('min_speakers', String(options.minSpeakers));
|
||||
if (options.maxSpeakers != null) formData.append('max_speakers', String(options.maxSpeakers));
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
headers: this.apiKey ? { 'X-API-Key': this.apiKey } : {},
|
||||
body: formData,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(`STT error ${response.status}: ${(error as { detail: string }).detail}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as TranscriptionResult;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if the STT service is healthy. */
|
||||
async health(): Promise<{ status: string; whisperx: boolean }> {
|
||||
const response = await fetch(`${this.baseUrl}/health`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return (await response.json()) as { status: string; whisperx: boolean };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import type { SynthesizeOptions, TTSVoice, TTSHealthResponse, GpuServiceConfig } from './types';
|
||||
import { resolveServiceUrl } from './resolve-url';
|
||||
|
||||
export class TtsClient {
|
||||
private baseUrl: string;
|
||||
private timeout: number;
|
||||
private apiKey?: string;
|
||||
|
||||
constructor(config: GpuServiceConfig) {
|
||||
this.baseUrl = resolveServiceUrl(config, 'tts');
|
||||
this.timeout = config.timeout ?? 30_000;
|
||||
this.apiKey = config.apiKey;
|
||||
}
|
||||
|
||||
/** Synthesize speech. Returns audio as ArrayBuffer. */
|
||||
async synthesize(options: SynthesizeOptions): Promise<{
|
||||
audio: ArrayBuffer;
|
||||
contentType: string;
|
||||
voice: string;
|
||||
duration: number;
|
||||
}> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/synthesize/auto`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.apiKey ? { 'X-API-Key': this.apiKey } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: options.text,
|
||||
voice: options.voice,
|
||||
speed: options.speed ?? 1.0,
|
||||
output_format: options.outputFormat ?? 'wav',
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(`TTS error ${response.status}: ${(error as { detail: string }).detail}`);
|
||||
}
|
||||
|
||||
return {
|
||||
audio: await response.arrayBuffer(),
|
||||
contentType: response.headers.get('content-type') ?? 'audio/wav',
|
||||
voice: response.headers.get('x-voice') ?? options.voice ?? 'default',
|
||||
duration: parseFloat(response.headers.get('x-duration') ?? '0'),
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get available voices. */
|
||||
async voices(): Promise<{ kokoro_voices: TTSVoice[]; custom_voices: TTSVoice[] }> {
|
||||
const response = await fetch(`${this.baseUrl}/voices`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return (await response.json()) as { kokoro_voices: TTSVoice[]; custom_voices: TTSVoice[] };
|
||||
}
|
||||
|
||||
/** Check if the TTS service is healthy. */
|
||||
async health(): Promise<TTSHealthResponse> {
|
||||
const response = await fetch(`${this.baseUrl}/health`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return (await response.json()) as TTSHealthResponse;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
// ============================================================================
|
||||
// STT Types
|
||||
// ============================================================================
|
||||
|
||||
export interface WordTimestamp {
|
||||
word: string;
|
||||
start: number;
|
||||
end: number;
|
||||
score?: number;
|
||||
speaker?: string;
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
speaker?: string;
|
||||
}
|
||||
|
||||
export interface TranscriptionResult {
|
||||
text: string;
|
||||
language?: string;
|
||||
model: string;
|
||||
latency_ms?: number;
|
||||
duration_seconds?: number;
|
||||
words?: WordTimestamp[];
|
||||
segments?: Segment[];
|
||||
speakers?: string[];
|
||||
}
|
||||
|
||||
export interface TranscribeOptions {
|
||||
language?: string;
|
||||
model?: string;
|
||||
/** Enable word-level timestamp alignment (default: true) */
|
||||
align?: boolean;
|
||||
/** Enable speaker diarization (default: false) */
|
||||
diarize?: boolean;
|
||||
minSpeakers?: number;
|
||||
maxSpeakers?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TTS Types
|
||||
// ============================================================================
|
||||
|
||||
export interface SynthesizeOptions {
|
||||
text: string;
|
||||
voice?: string;
|
||||
speed?: number;
|
||||
outputFormat?: 'wav' | 'mp3';
|
||||
}
|
||||
|
||||
export type TTSVoiceType = 'kokoro' | 'piper' | 'edge' | 'f5_custom';
|
||||
|
||||
export interface TTSVoice {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: TTSVoiceType;
|
||||
}
|
||||
|
||||
export interface TTSHealthResponse {
|
||||
status: string;
|
||||
service: string;
|
||||
models_loaded: Record<string, boolean>;
|
||||
auth_required: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Image Generation Types
|
||||
// ============================================================================
|
||||
|
||||
export interface GenerateImageOptions {
|
||||
prompt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
steps?: number;
|
||||
seed?: number;
|
||||
outputFormat?: 'png' | 'jpg';
|
||||
}
|
||||
|
||||
export interface GenerateImageResult {
|
||||
success: boolean;
|
||||
image_url: string;
|
||||
prompt: string;
|
||||
width: number;
|
||||
height: number;
|
||||
steps: number;
|
||||
seed: number;
|
||||
generation_time: number;
|
||||
}
|
||||
|
||||
export interface ImageGenHealthResponse {
|
||||
status: string;
|
||||
service: string;
|
||||
flux_available: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GPU Service Config
|
||||
// ============================================================================
|
||||
|
||||
export interface GpuServiceConfig {
|
||||
/**
|
||||
* Base URL of the GPU server.
|
||||
*
|
||||
* LAN mode (single host, different ports):
|
||||
* `http://192.168.178.11` → :3025, :3020, :3022, :3023
|
||||
*
|
||||
* Public mode (different hostnames):
|
||||
* `https://gpu.mana.how` → gpu-llm.mana.how, gpu-stt.mana.how, etc.
|
||||
*/
|
||||
baseUrl: string;
|
||||
/** Override individual service URLs (takes precedence over baseUrl) */
|
||||
urls?: {
|
||||
llm?: string;
|
||||
stt?: string;
|
||||
tts?: string;
|
||||
image?: string;
|
||||
ollama?: string;
|
||||
};
|
||||
/** API key for authenticated access (X-API-Key header) */
|
||||
apiKey?: string;
|
||||
/** Request timeout in ms (default: 30000) */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/** Default public URLs */
|
||||
export const GPU_PUBLIC_URLS = {
|
||||
llm: 'https://gpu-llm.mana.how',
|
||||
stt: 'https://gpu-stt.mana.how',
|
||||
tts: 'https://gpu-tts.mana.how',
|
||||
image: 'https://gpu-img.mana.how',
|
||||
ollama: 'https://gpu-ollama.mana.how',
|
||||
} as const;
|
||||
|
||||
/** Default LAN URLs */
|
||||
export const GPU_LAN_URLS = {
|
||||
llm: 'http://192.168.178.11:3025',
|
||||
stt: 'http://192.168.178.11:3020',
|
||||
tts: 'http://192.168.178.11:3022',
|
||||
image: 'http://192.168.178.11:3023',
|
||||
ollama: 'http://192.168.178.11:11434',
|
||||
} as const;
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"esModuleInterop": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue