@series-inc/stowkit-cli 0.6.36 → 0.6.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai-tagger.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { GoogleGenAI } from '@google/genai';
2
2
  import { AssetType } from './core/types.js';
3
- const MODEL = 'gemini-2.5-flash-lite-preview';
3
+ const MODEL = 'gemini-3.1-flash-lite-preview';
4
4
  const ASSET_TYPE_LABELS = {
5
5
  [AssetType.Texture2D]: 'texture',
6
6
  [AssetType.StaticMesh]: '3D mesh',
package/dist/server.js CHANGED
@@ -2196,6 +2196,94 @@ async function handleRequest(req, res, staticApps) {
2196
2196
  }
2197
2197
  return;
2198
2198
  }
2199
+ // POST /api/ai/tag-batch — AI-tag specific assets by ID, concurrently
2200
+ if (pathname === '/api/ai/tag-batch' && req.method === 'POST') {
2201
+ if (!projectConfig) {
2202
+ json(res, { error: 'No project open' }, 400);
2203
+ return;
2204
+ }
2205
+ const apiKey = projectConfig.config.ai?.geminiApiKey;
2206
+ if (!apiKey) {
2207
+ json(res, { error: 'Gemini API key not configured' }, 400);
2208
+ return;
2209
+ }
2210
+ try {
2211
+ const body = JSON.parse(await readBody(req));
2212
+ const ids = body.ids;
2213
+ if (!Array.isArray(ids) || ids.length === 0) {
2214
+ json(res, { error: 'ids must be a non-empty array' }, 400);
2215
+ return;
2216
+ }
2217
+ const { tagAsset: aiTagAsset } = await import('./ai-tagger.js');
2218
+ const { readManifest, readThumbnailFile } = await import('./app/thumbnail-cache.js');
2219
+ const manifest = await readManifest(projectConfig.srcArtDir);
2220
+ const eligible = assets.filter(a => ids.includes(a.id) &&
2221
+ a.status === 'ready' &&
2222
+ !a.settings.excluded &&
2223
+ (!a.settings.tags || a.settings.tags.length === 0) &&
2224
+ a.type !== AssetType.MaterialSchema &&
2225
+ !a.parentId);
2226
+ let tagged = 0;
2227
+ let skipped = ids.length - eligible.length;
2228
+ const errors = [];
2229
+ const CONCURRENCY = 20;
2230
+ // Process in batches of CONCURRENCY
2231
+ for (let i = 0; i < eligible.length; i += CONCURRENCY) {
2232
+ const batch = eligible.slice(i, i + CONCURRENCY);
2233
+ const results = await Promise.allSettled(batch.map(async (asset) => {
2234
+ let inputData;
2235
+ let mimeType;
2236
+ if (asset.type === AssetType.Audio) {
2237
+ const ext = asset.fileName.split('.').pop()?.toLowerCase() ?? 'wav';
2238
+ const mimeMap = {
2239
+ wav: 'audio/wav', mp3: 'audio/mpeg', ogg: 'audio/ogg',
2240
+ flac: 'audio/flac', aac: 'audio/aac', m4a: 'audio/mp4',
2241
+ };
2242
+ mimeType = mimeMap[ext] ?? 'audio/wav';
2243
+ const raw = await readFile(projectConfig.srcArtDir, asset.id);
2244
+ if (!raw)
2245
+ throw new Error('Source file not found');
2246
+ inputData = Buffer.from(raw);
2247
+ }
2248
+ else {
2249
+ const entry = manifest[asset.stringId];
2250
+ if (!entry)
2251
+ throw new Error('No thumbnail cached');
2252
+ const thumbData = await readThumbnailFile(projectConfig.srcArtDir, asset.stringId, entry.format);
2253
+ if (!thumbData)
2254
+ throw new Error('Thumbnail file missing');
2255
+ inputData = thumbData;
2256
+ const formatMime = {
2257
+ png: 'image/png', webp: 'image/webp', webm: 'video/webm',
2258
+ };
2259
+ mimeType = formatMime[entry.format] ?? 'image/webp';
2260
+ }
2261
+ const newTags = await Promise.race([
2262
+ aiTagAsset(apiKey, asset.type, asset.fileName, inputData, mimeType),
2263
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Gemini request timed out (30s)')), 30_000)),
2264
+ ]);
2265
+ asset.settings = { ...asset.settings, tags: newTags };
2266
+ if (!asset.parentId) {
2267
+ const meta = assetSettingsToStowmeta(asset);
2268
+ await writeStowmeta(projectConfig.srcArtDir, asset.id, meta);
2269
+ }
2270
+ broadcast({ type: 'asset-update', id: asset.id, updates: { settings: asset.settings } });
2271
+ return asset.id;
2272
+ }));
2273
+ for (const r of results) {
2274
+ if (r.status === 'fulfilled')
2275
+ tagged++;
2276
+ else
2277
+ errors.push({ id: 'unknown', error: r.reason?.message ?? String(r.reason) });
2278
+ }
2279
+ }
2280
+ json(res, { tagged, skipped, errors });
2281
+ }
2282
+ catch (err) {
2283
+ json(res, { error: err.message }, 500);
2284
+ }
2285
+ return;
2286
+ }
2199
2287
  // GET /api/thumbnails/cached — return which thumbnails are still valid in the cache
2200
2288
  if (pathname === '/api/thumbnails/cached' && req.method === 'GET') {
2201
2289
  if (!projectConfig) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/stowkit-cli",
3
- "version": "0.6.36",
3
+ "version": "0.6.37",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "stowkit": "./dist/cli.js"
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "@google/genai": "^1.46.0",
22
22
  "@series-inc/stowkit-editor": "^0.1.8",
23
- "@series-inc/stowkit-packer-gui": "^0.1.23",
23
+ "@series-inc/stowkit-packer-gui": "^0.1.24",
24
24
  "@strangeape/ffmpeg-audio-wasm": "^0.1.0",
25
25
  "draco3d": "^1.5.7",
26
26
  "fbx-parser": "^2.1.3",