openvibers/openvibe.ai Commits

Add low cost Meme caption workflow (#384)

OP OpenVibers committed 4 days ago · fa1aba70e2 · 3 files, +26 −2

Changes

ddiff --git a/server/workflows/meme.js b/server/workflows/meme.js
nnew file mode 100644
iindex 0000000..7906e92
---- /dev/null
++++ b/server/workflows/meme.js
@@@ -0,0 +1,20 @@
++'use strict';
++
++const STR = (n) => ({ type: 'string', maxLength: n });
++const template = {
++ key: 'meme.caption', name: 'Meme: caption ideas', owner: 'meme', visibility: 'internal',
++ description: 'Write a few original, short caption ideas for a known meme format.',
++ input_schema: { type: 'object', additionalProperties: false, required: ['format', 'topic'], properties: { format: STR(120), topic: STR(300) } },
++ output_schema: { type: 'object', additionalProperties: false, required: ['ideas'], properties: { ideas: { type: 'array', minItems: 3, maxItems: 5, items: STR(480) } } },
++ system_prompt: 'You write original meme captions. Never quote a known meme, copy a template caption, use hateful slurs, or invent factual claims. Keep each idea short, varied, and safe for a general audience. Return three to five distinct caption ideas as JSON.',
++ user_prompt: 'Meme format: {{format}}\nTopic or situation: {{topic}}\n\nWrite three to five original caption ideas. Each idea may use up to four short lines separated by a newline. Return JSON with an ideas array only.',
++ default_route: 'meme.caption', metadata: { json_name: 'ideas' },
++};
++const workflow = {
++ key: 'meme.caption', name: 'Original meme caption ideas', namespace: 'meme', owner: 'meme',
++ description: 'Generate a handful of original, short caption ideas for a selected meme template.',
++ input_schema: template.input_schema, output_schema: template.output_schema, default_route: 'meme.caption',
++ steps: [{ kind: 'llm', template: 'meme.caption', operation: 'generate', output: 'json', params: { max_tokens: 700, temperature: 0.85 } }],
++};
++
++module.exports = { templates: [template], workflows: [workflow] };
ddiff --git a/server/workflows/seed.js b/server/workflows/seed.js
iindex 2b21e62..93b8ec9 100644
---- a/server/workflows/seed.js
++++ b/server/workflows/seed.js
@@@ -21,6 +21,7 @@ const core = require('./core');
const products = require('./products');
const chat = require('./chat');
const research = require('./research');
++const meme = require('./meme');
const poolSeed = require('../providers/pool-seed');
const { resolveSecret } = require('../util');
@@@ -203,6 +204,8 @@ async function seed({ registry, quotas, config, env = process.env, db }) {
// admin adds as kind anthropic. The others are skipped as unsupported, never converted.
await registry.seedVersioned('route', 'default.messages', pool(120000, { constraints: { objective: 'correctness', latency_class: 'interactive' }, max_output_tokens: 8192 }));
await registry.seedVersioned('route', 'default.json', pool(60000, { options: { temperature: 0.3 }, max_output_tokens: 2400, response_format: 'json' }));
++ // Meme captions need fluency, not factual precision; choose the cheapest eligible JSON route by default.
++ await registry.seedVersioned('route', 'meme.caption', pool(30000, { constraints: { objective: 'cheapest', latency_class: 'interactive' }, options: { temperature: 0.85 }, max_output_tokens: 700, response_format: 'json' }));
await registry.seedVersioned('route', 'default.embedding', pool(30000, { 'capability': 'embed', constraints: { objective: 'balanced' } }));
// media.analyze (WS-O task 5): the local model when there is one; the paid route is used only under a media.paid budget.
// Both are background work (BA1): nobody watches a VOD's overview arrive, so the route says it may wait.
@@@ -255,11 +258,11 @@ async function seed({ registry, quotas, config, env = process.env, db }) {
await registry.seedVersioned('route', 'chat.member', { primary: { provider: 'shared', model: null }, fallbacks: [], options: {}, max_output_tokens: 600, response_format: 'text', timeout_ms: null, alias_of: 'default.chat' });
// Templates, then workflows (workflows reference templates)
-- for (const t of [...live.templates, ...core.templates, ...products.templates, ...media.templates, ...chat.templates, ...space.templates, ...research.templates]) {
++ for (const t of [...live.templates, ...core.templates, ...products.templates, ...media.templates, ...chat.templates, ...space.templates, ...research.templates, ...meme.templates]) {
const { key, ...def } = t;
await registry.seedVersioned('template', key, { description: null, default_route: null, owner: 'ai', visibility: 'internal', metadata: {}, ...def });
}
-- for (const w of [...live.workflows, ...core.workflows, ...products.workflows, ...media.workflows, ...chat.workflows, ...space.workflows, ...research.workflows]) {
++ for (const w of [...live.workflows, ...core.workflows, ...products.workflows, ...media.workflows, ...chat.workflows, ...space.workflows, ...research.workflows, ...meme.workflows]) {
const { key, ...def } = w;
await registry.seedVersioned('workflow', key, { description: null, default_route: null, cache_ttl_sec: null, metadata: {}, ...def });
}
ddiff --git a/test/workflows.test.js b/test/workflows.test.js
iindex e4d117f..cd710e8 100644
---- a/test/workflows.test.js
++++ b/test/workflows.test.js
@@@ -14,6 +14,7 @@ const SRC = (n) => Array.from({ length: n }, (_, i) => ({ source_type: 'news.art
const PASS = { role: 'chat', kind: 'chat_global', system: [{ text: 'You summarize chat.', cache: true }], user: 'Summarize: hi all', max_tokens: 80 };
const EXAMPLES = {
++ 'meme.caption': { format: 'Two choices', topic: 'choosing whether to fix the bug before lunch' },
'ai.messages': { max_tokens: 64, messages: [{ role: 'user', content: 'Say hello' }] },
'live.translate': { text: 'こんにちは、みなさん', from: 'ja', to: 'en', context: 'chat' },
'live.paste.describe_image': { title: 'my screenshot', image: { data_url: PNG } },