openvibers/openvibe.ai Commits

AI: chat.persona.image, a Vibe persona's avatar (#503)

OP OpenVibers committed 18 h ago · 3e440af549 · 2 files, +21 −1

The same image step and route as ai.image (default.image), in Chat's namespace so Chat's grant can run it
for Vibe personas (OpenVibe.Chat #546). Pictures are off by default; nothing calls it until the owner turns
them on and an image model is configured.

Changes

ddiff --git a/server/workflows/chat.js b/server/workflows/chat.js
iindex 4983a10..11b0a0e 100644
---- a/server/workflows/chat.js
++++ b/server/workflows/chat.js
@@@ -17,6 +17,10 @@
* included; Chat runs the tools and sends the results in the next turn, a few turns at most.
* The same passthrough as ai.complete (the prompt is not kept), in Chat's namespace and on its
* route, so Chat's grant and quota apply; with a person's own key it goes to their provider.
++ *
++ * chat.persona.image the small avatar for a Vibe conversation's persona (the owner, 2026-11-12). The same image
++ * step and route as ai.image (route default.image), in Chat's namespace so Chat's grant can
++ * run it; the picture comes back inline (b64) or as a URL, and Chat stores the bytes in Media.
*/
const core = require('./core');
@@@ -85,6 +89,21 @@ const workflows = [
steps: [{ kind: 'transcribe', route: 'live.stt' }],
cache_mode: 'none', metadata: { seed: true, retain_input: false },
},
++ {
++ key: 'chat.persona.image', name: 'Chat: a Vibe persona\'s avatar', namespace: 'chat',
++ description: 'The small avatar for a Vibe conversation\'s persona: the same image step and route as ai.image (default.image), run under Chat\'s own namespace and grant. The picture comes back inline or as a URL; Chat stores it in Media.',
++ input_schema: OBJ({
++ model: STR(256),
++ prompt: STR(1200),
++ size: { type: 'string', pattern: '^[0-9]{3,4}x[0-9]{3,4}$' },
++ n: { type: 'integer', minimum: 1, maximum: 1 },
++ max_cost_usd: { type: ['number', 'string', 'null'] },
++ }, ['prompt']),
++ output_schema: OBJ({ images: { type: 'array', minItems: 1, items: OBJ({ b64: { type: ['string', 'null'] }, url: { type: ['string', 'null'] } }) }, count: { type: 'integer', minimum: 1 } }, ['images', 'count']),
++ // No cache: several MB of base64 per run, and a persona's face is made once for a chat.
++ steps: [{ kind: 'image', route: 'default.image' }], cache_mode: 'none',
++ metadata: { seed: true, retain_input: false },
++ },
{
key: 'chat.member.turn', name: 'Chat: an AI member\'s turn, with tools', namespace: 'chat',
description: 'One turn of a room\'s AI member as a Chat Completions request with the tools Chat runs (network search, the time, dice, a household\'s pantry); the choices come back with any tool calls. The prompt is not kept.',
ddiff --git a/test/workflows.test.js b/test/workflows.test.js
iindex 426da5d..181e486 100644
---- a/test/workflows.test.js
++++ b/test/workflows.test.js
@@@ -69,6 +69,7 @@ const EXAMPLES = {
'pics.describe_image': { image: { data_url: PNG }, title: 'The coast' },
'pics.orientation': { image: { data_url: PNG } },
'chat.member.turn': { messages: [{ role: 'system', content: 'You are Pantry.' }, { role: 'user', content: 'kim: @Pantry roll a die' }], tools: [{ type: 'function', function: { name: 'roll_dice', parameters: { type: 'object', properties: { sides: { type: 'integer' } } } } }] },
++ 'chat.persona.image': { prompt: 'A small square avatar for a chat called Keyboard Scout about mechanical keyboards: one bold keyboard, flat vector, no text.', size: '256x256', n: 1 },
'tools.describe': { tool: 'img.convert', sources: [{ source_type: 'tools.tool', title: 'img.convert descriptor', content: '{"id":"img.convert","inputs":["file","format"]}' }] },
};
for (const k of new Set(Object.values(KIND_TO_WORKFLOW).concat('live.complete'))) EXAMPLES[k] = PASS;
@@@ -136,7 +137,7 @@ t.test('boot seeds every required workflow key', async () => {
h = await boot({ env: { OV_MEDIA_INTERNAL_URL: mediaBase, AI_IMAGE_MODEL: 'gpt-image-1' } });
const keys = (await h.registry.listWorkflows()).map(w => w.key);
for (const k of ['content.wiki', 'wiki.generate_space', 'wiki.generate_page', 'blog.draft_post', 'blog.write_digest', 'blog.write_satire', 'news.summarize_story', 'news.compare_perspectives', 'reviews.summarize_entity', 'deals.enrich_deal',
-- 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes', 'food.read_pantry_photo', 'pics.describe_image', 'rent.screen_listing', 'chat.member.reply', 'chat.member.turn', 'chat.vibe.listen',
++ 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes', 'food.read_pantry_photo', 'pics.describe_image', 'rent.screen_listing', 'chat.member.reply', 'chat.member.turn', 'chat.vibe.listen', 'chat.persona.image',
'live.translate', 'live.stream.describe_frame', 'live.stream.summarize', 'live.viewers.reply', 'live.chat.insight', 'live.hero.slogans', 'live.stream.recap', 'network.site_copy']) {
assert.ok(keys.includes(k), `missing ${k}`);
}