yesilcam-studios 0.1.0 → 0.3.0

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.
@@ -0,0 +1,75 @@
1
+ // Design Studio: baskı ve dijital tasarım (projeler → tasarımlar), üretim, dışa aktarma.
2
+ import { z, op, ID, NEG, n, clean, quote } from './_util.js'
3
+ import { uploadAsset } from '../files.js'
4
+
5
+ export const PRESETS = {
6
+ print: { a6: [105, 148], a5: [148, 210], a4: [210, 297], a3: [297, 420], a2: [420, 594], a1: [594, 841], a0: [841, 1189], card: [85, 55], postcard: [148, 105], flyer_dl: [99, 210], poster50: [500, 700], poster70: [700, 1000], rollup: [850, 2000] },
7
+ digital: { ig_post: [1080, 1350], ig_square: [1080, 1080], story: [1080, 1920], fb_cover: [1640, 624], yt_cover: [2560, 1440], yt_thumb: [1280, 720], x_post: [1600, 900], li_post: [1200, 627], web_banner: [1920, 600], slide: [1920, 1080], pinterest: [1000, 1500], email: [600, 300] },
8
+ }
9
+ const PRESET_IDS = [...Object.keys(PRESETS.print), ...Object.keys(PRESETS.digital)]
10
+ const STYLES = ['minimalism', 'maximalism', 'futuristic', 'vector', 'collage', 'retro', 'cyberpunk', 'pop-art', 'glassmorphism', 'clay', 'pixel-art', 'editorial', 'y2k', 'swiss', 'surreal', 'bohemian', 'victorian', 'graffiti', 'aurora', 'handwritten', 'ink-wash']
11
+ export function canvasOf(a) {
12
+ if (a.preset) {
13
+ if (PRESETS.print[a.preset]) { const [w, h] = PRESETS.print[a.preset]; return { kind: 'print', unit: 'mm', width: w, height: h, dpi: 300, bleed_mm: 3, preset: a.preset } }
14
+ const [w, h] = PRESETS.digital[a.preset]; return { kind: 'digital', unit: 'px', width: w, height: h, dpi: 72, preset: a.preset }
15
+ }
16
+ if (a.width && a.height) return a.unit && a.unit !== 'px' ? { kind: 'print', unit: a.unit, width: a.width, height: a.height, dpi: 300, bleed_mm: 3 } : { kind: 'digital', unit: 'px', width: a.width, height: a.height, dpi: 72 }
17
+ return null
18
+ }
19
+
20
+ export default [
21
+ op({ name: 'design_presets', group: 'design', action: 'presets', kind: 'local', summary: 'Design canvas presets and styles',
22
+ description: 'Print (mm, 300 dpi, 3 mm bleed) and digital (px) canvas presets and the 21 design styles of Design Studio.', input: {},
23
+ run: () => ({ presets: PRESETS, styles: STYLES }) }),
24
+ op({ name: 'design_projects', group: 'design', action: 'projects', kind: 'read', summary: 'Design Studio projects (client folders)',
25
+ description: 'Design Studio projects (one folder per client) with design counts.', input: {},
26
+ run: (c) => c.call('ys-design-project-list') }),
27
+ op({ name: 'design_list', group: 'design', action: 'list', kind: 'read', summary: 'Designs in a Design Studio project',
28
+ description: 'Designs inside a project (canvas, style, status, thumbnail).', input: { project_id: ID('Project id') }, positional: 'project_id',
29
+ run: (c, a) => c.call('ys-design-list', { project_id: Number(a.project_id) }) }),
30
+ op({ name: 'design_generate', group: 'design', action: 'generate', kind: 'spend', summary: 'Design Studio: design a poster, card, brochure or social post',
31
+ description: 'Creates a print or digital design from a brief: canvas preset (a4, card, poster50, ig_post, story …) or custom size, style, texts, your images and logo. quality draft / print4k / premium4k. Or improve (instruction) / upgrade an existing design (design_id). The estimate includes a redraw reserve that is refunded if unused. Export with yesilcam_design_export.',
32
+ positional: 'prompt',
33
+ input: { prompt: z.string().max(2000).optional().describe('What to design (brief)'), design_id: ID('Existing design (improve / upgrade)').optional(), mode: z.enum(['new', 'improve', 'upgrade']).optional(),
34
+ instruction: z.string().max(1500).optional().describe('Change request (improve)'), preset: z.enum(PRESET_IDS).optional(), width: z.number().optional(), height: z.number().optional(), unit: z.enum(['mm', 'cm', 'in', 'px']).optional(),
35
+ style: z.enum(STYLES).optional(), quality: z.enum(['draft', 'print4k', 'premium4k']).optional(), brand: z.string().max(120).optional(), lang: z.string().length(2).optional(),
36
+ texts: z.array(z.string().max(600)).max(12).optional().describe('Exact texts to place (headline first)'), image_asset_ids: z.array(z.union([z.string(), z.number()])).max(8).optional(),
37
+ image_paths: z.array(z.string()).max(8).optional().describe('Local images to upload and place'), logo_asset_id: ID('Logo image asset id').optional(),
38
+ project_id: ID('Project to put a new design in').optional(), title: z.string().max(160).optional(), negative_prompt: NEG },
39
+ prepare: (c, a) => {
40
+ const mode = a.mode || (a.design_id ? 'improve' : 'new')
41
+ if (mode === 'new' && !a.prompt && !(a.texts || []).length) throw new Error('give a prompt (brief) or texts')
42
+ if (mode === 'improve' && !a.instruction) throw new Error('instruction is required to improve a design')
43
+ const canvas = mode === 'new' ? canvasOf(a) : null
44
+ if (mode === 'new' && !canvas && !a.design_id) throw new Error('give a preset or width + height')
45
+ return { mode, canvas, quality: a.quality || 'print4k', images: (a.image_asset_ids || []).length + (a.image_paths || []).length + (a.logo_asset_id ? 1 : 0) }
46
+ },
47
+ quote: async (c, a, p) => {
48
+ const r = await c.call('ys-design-quote', clean({ mode: p.mode, images: p.images, text_len: (a.prompt || '').length + (a.texts || []).join('').length, integrate: Math.min(3, p.images), design_id: n(a.design_id), canvas: p.canvas || undefined }))
49
+ const needImage = p.mode !== 'improve'
50
+ return quote(c, [p.mode !== 'upgrade' && { item: 'layout assistant', tokens: r.sam }, needImage && { item: `image ${p.quality}`, tokens: r.image?.[p.quality] }, { item: 'fidelity check', tokens: r.check }, r.reserve && { item: 'redraw reserve (refunded if unused)', tokens: r.reserve[p.quality] }],
51
+ { balance: n(r.balance) })
52
+ },
53
+ run: async (c, a, ctx, p) => {
54
+ let id = a.design_id ? Number(a.design_id) : null
55
+ if (!id) { const d = await c.call('ys-design-create', clean({ project_id: n(a.project_id), title: a.title, canvas: p.canvas, style: a.style })); id = Number(d.design_id) }
56
+ const images = (a.image_asset_ids || []).map((x) => ({ asset_id: Number(x), role: 'image', treatment: 'integrate' }))
57
+ for (const path of a.image_paths || []) images.push({ asset_id: Number((await uploadAsset(c, path, { kind: 'image' })).asset_id), role: 'image', treatment: 'integrate' })
58
+ if (a.logo_asset_id) images.push({ asset_id: Number(a.logo_asset_id), role: 'logo', treatment: 'cutout' })
59
+ const brief = p.mode === 'new' ? clean({ prompt: a.prompt, brand: a.brand, lang: a.lang, images, texts: (a.texts || []).map((t, i) => ({ text: t, role: i === 0 ? 'headline' : 'body' })), negative_prompt: a.negative_prompt }) : undefined
60
+ const r = await c.call('ys-design-generate', clean({ design_id: id, mode: p.mode, quality: p.quality, brief, instruction: a.instruction }))
61
+ return { design_id: String(id), ...r }
62
+ },
63
+ wait: (r) => ({ kind: 'design', id: r.design_id }) }),
64
+ op({ name: 'design_get', group: 'design', action: 'get', kind: 'read', summary: 'A design with its status',
65
+ description: 'Design status, canvas, layers and job progress.', input: { design_id: ID('Design id') }, positional: 'design_id',
66
+ run: async (c, a) => { const r = await c.call('ys-design-get', { design_id: Number(a.design_id) }); const d = r.design || r; if (d?.doc) d.doc = { layers: (d.doc.layers || []).map((l) => clean({ id: l.id, type: l.type, name: l.name, text: l.text })) }; return d } }),
67
+ op({ zeroRunsFree: true, name: 'design_export', group: 'design', action: 'export', kind: 'spend', summary: 'Export a design: PNG, JPG, WebP, print PDF (CMYK, PDF/X), SVG or EPS',
68
+ description: 'Exports a design. PNG/JPG/WebP are free; PDF/SVG/EPS, CMYK and PDF/X need a Basic/Pro plan and cost a small fee. Free plan exports over 720 px get a watermark. Use with wait to download the file.',
69
+ input: { design_id: ID('Design id'), format: z.enum(['png', 'jpg', 'webp', 'pdf', 'svg', 'eps']).optional(), size: z.union([z.literal('full'), z.number().int().min(64).max(12000)]).optional(),
70
+ cmyk: z.boolean().optional(), pdfx: z.enum(['x4', 'x1a']).optional(), profile: z.enum(['fogra39', 'fogra51']).optional(), bleed: z.boolean().optional(), marks: z.boolean().optional() },
71
+ positional: 'design_id',
72
+ quote: async (c, a) => { const pro = ['pdf', 'svg', 'eps'].includes(a.format) || a.cmyk || a.pdfx; if (!pro) return quote(c, [{ item: 'export', tokens: 0 }]); const r = await c.call('ys-design-quote', { design_id: Number(a.design_id) }); return quote(c, [{ item: 'design_export_pro', tokens: r.export_pro }], { balance: n(r.balance) }) },
73
+ run: (c, a) => c.call('ys-design-export', clean({ design_id: Number(a.design_id), format: a.format || 'png', size: a.size || 'full', colorspace: a.cmyk || a.pdfx ? 'cmyk' : undefined, pdfx: a.pdfx, profile: a.profile, bleed: a.bleed, marks: a.marks })),
74
+ wait: (r) => ({ kind: 'design_export', id: r.export_id }) }),
75
+ ]
@@ -0,0 +1,51 @@
1
+ // Fashion Try-On: giysi deneme (tek parça / kombin), beden varyantı, stilist, moda filmi, gardırop ve modeller.
2
+ import { z, op, ID, NEG, n, clean, opCost, quote } from './_util.js'
3
+ import { uploadAsset } from '../files.js'
4
+
5
+ const CAT = z.enum(['dress', 'top', 'bottom', 'outerwear', 'shoes', 'accessory'])
6
+ const person = (a) => clean({ global_model_id: n(a.model_id), character_id: n(a.character_id), source_asset_id: n(a.person_asset_id) })
7
+
8
+ export default [
9
+ op({ name: 'tryon', group: 'fashion', action: 'tryon', kind: 'spend', summary: 'Fashion Try-On: dress a person, model or character in your garments',
10
+ description: 'Puts one garment, or an outfit of up to 5 garments (one per category), on a ready-made model (model_id), your character, or a person photo. Garments can be asset ids or local files. Modest and headscarf options available. One try-on fee per garment.',
11
+ input: { garment_asset_id: ID('Garment image asset id').optional(), garment_path: z.string().optional(), outfit: z.array(z.object({ asset_id: z.union([z.string(), z.number()]), category: CAT })).max(5).optional().describe('Outfit: up to 5 garments'),
12
+ model_id: ID('Ready-made model id (fashion_models)').optional(), character_id: ID('Your character id').optional(), person_asset_id: ID('Person photo asset id').optional(), person_path: z.string().optional(),
13
+ prompt: z.string().max(500).optional().describe('Style note'), aspect: z.enum(['1:1', '16:9', '9:16', '3:4', '4:3']).optional(), modest: z.boolean().optional(), headscarf: z.boolean().optional(), negative_prompt: NEG },
14
+ prepare: (c, a) => { if (!a.model_id && !a.character_id && !a.person_asset_id && !a.person_path) throw new Error('give model_id, character_id, person_asset_id or person_path'); if (!a.garment_asset_id && !a.garment_path && !a.outfit?.length) throw new Error('give a garment or an outfit'); return { count: a.outfit?.length || 1 } },
15
+ quote: async (c, a, p) => { const x = await opCost(c, 'tryon'); return quote(c, [{ item: `tryon x${p.count}`, tokens: x.tokens * p.count }], { balance: x.balance }) },
16
+ run: async (c, a) => {
17
+ const pa = { ...a }; if (a.person_path && !a.person_asset_id) pa.person_asset_id = (await uploadAsset(c, a.person_path, { kind: 'image' })).asset_id
18
+ const base = clean({ ...person(pa), prompt: a.prompt, aspect_ratio: a.aspect || '3:4', quality_tier: 'standard', origin: 'fashion_tryon', modest: a.modest ? 1 : undefined, headscarf: a.headscarf ? 1 : undefined, negative_prompt: a.negative_prompt })
19
+ if (a.outfit?.length) return c.call('aistudio-create-job', { job_type: 'tryon_outfit', ...base, garments: a.outfit.map((g) => ({ asset_id: Number(g.asset_id), category: g.category })) })
20
+ const g = a.garment_asset_id || (await uploadAsset(c, a.garment_path, { kind: 'image' })).asset_id
21
+ return c.call('aistudio-create-job', { job_type: 'tryon', ...base, garment_asset_id: Number(g) })
22
+ },
23
+ wait: (r) => ({ kind: 'job', id: r.job_id }) }),
24
+ op({ name: 'body_variant', group: 'fashion', action: 'body', kind: 'spend', summary: 'Fashion: show a look on a different body type',
25
+ description: 'Shows the same person (person_asset_id) or a garment (garment_asset_id + gender) on another body type: petite, standard, curvy, plus, athletic, tall.',
26
+ input: { body_type: z.enum(['petite', 'standard', 'curvy', 'plus', 'athletic', 'tall']), person_asset_id: ID('Person image').optional(), garment_asset_id: ID('Garment image').optional(), gender: z.enum(['female', 'male', 'unisex']).optional(), age: z.enum(['young', 'adult', 'mature']).optional(), aspect: z.enum(['1:1', '3:4', '9:16']).optional() },
27
+ quote: async (c) => { const x = await opCost(c, 'tryon'); return quote(c, [{ item: 'tryon', tokens: x.tokens }], { balance: x.balance }) },
28
+ run: (c, a) => c.call('aistudio-create-job', clean({ job_type: 'body_variant', body_type: a.body_type, source_asset_id: n(a.person_asset_id), garment_asset_id: n(a.garment_asset_id), gender: a.gender, age: a.age, aspect_ratio: a.aspect || '3:4', quality_tier: 'standard', origin: 'fashion_tryon' })),
29
+ wait: (r) => ({ kind: 'job', id: r.job_id }) }),
30
+ op({ name: 'stylist', group: 'fashion', action: 'stylist', kind: 'spend', summary: 'Stylist: pick an outfit from your wardrobe for an occasion and try it on',
31
+ description: 'Picks an outfit for an occasion from your newest wardrobe garments and tries it on (your signature character by default). Charged one try-on per chosen garment (up to 5).',
32
+ positional: 'occasion',
33
+ input: { occasion: z.string().min(2).max(300), character_id: ID('Character').optional(), model_id: ID('Ready-made model').optional(), person_asset_id: ID('Person photo').optional(), modest: z.boolean().optional(), headscarf: z.boolean().optional(), negative_prompt: NEG },
34
+ quote: async (c) => { const x = await opCost(c, 'tryon'); return quote(c, [{ item: 'tryon x up to 5 (one per chosen garment)', tokens: x.tokens * 5 }], { balance: x.balance, approximate: true, notes: 'Upper bound; you pay one try-on per garment the stylist picks.' }) },
35
+ run: (c, a) => c.call('aistudio-stylist-create', clean({ occasion: a.occasion, ...person(a), aspect_ratio: '3:4', modest: a.modest ? 1 : undefined, headscarf: a.headscarf ? 1 : undefined, negative_prompt: a.negative_prompt })),
36
+ wait: (r) => ({ kind: 'job', id: r.job_id }) }),
37
+ op({ name: 'fashion_film', group: 'fashion', action: 'film', kind: 'spend', summary: 'Fashion film from a try-on result (Pro)',
38
+ description: 'Turns a try-on result image into a 2-4 scene fashion film with music and optional voice-over (Pro plan).',
39
+ input: { source_asset_id: ID('Try-on result image asset id'), scene_count: z.number().int().min(2).max(4).optional(), music_vibe: z.string().max(80).optional(), aspect: z.enum(['9:16', '16:9', '1:1']).optional(),
40
+ tier: z.enum(['draft', 'standard', 'cinema', 'cinemapro']).optional(), voiceover: z.string().max(2000).optional(), voice_id: z.string().optional(), brand: z.string().max(80).optional(), title: z.string().max(120).optional(), negative_prompt: NEG },
41
+ quote: async (c, a) => { const k = a.scene_count || 3; const v = await opCost(c, `video_${a.tier || 'standard'}_5s`); const m = await opCost(c, 'music_track'); return quote(c, [{ item: `${v.key} x${k}`, tokens: v.tokens * k }, { item: 'music_track', tokens: m.tokens }], { balance: v.balance }) },
42
+ run: (c, a) => c.call('aistudio-fashion-film-create', clean({ source_asset_id: Number(a.source_asset_id), mode: a.brand ? 'smart' : 'auto', scene_count: a.scene_count || 3, music_vibe: a.music_vibe, aspect_ratio: a.aspect || '9:16', tier: a.tier || 'standard', voiceover_text: a.voiceover, voice_id: a.voice_id, brand: a.brand, title: a.title, negative_prompt: a.negative_prompt })),
43
+ wait: (r) => ({ kind: 'flow', id: r.flow_id }) }),
44
+ op({ name: 'wardrobe', group: 'fashion', action: 'wardrobe', kind: 'read', summary: 'Your garments (wardrobe)', description: 'Garments in your wardrobe (with product link and price when set).',
45
+ input: { limit: z.number().int().min(1).max(100).optional() }, run: (c, a) => c.call('aistudio-garment-list', { limit: a.limit || 60 }) }),
46
+ op({ name: 'garment_from_url', group: 'fashion', action: 'import', kind: 'write', summary: 'Import a garment from a product page URL',
47
+ description: 'Reads a shop product page, takes its main image and adds it to your wardrobe. Free.', input: { url: z.string().url() }, positional: 'url',
48
+ run: (c, a) => c.call('aistudio-garment-from-url', { url: a.url }) }),
49
+ op({ name: 'fashion_models', group: 'fashion', action: 'models', kind: 'read', summary: 'Ready-made models and your own models', description: 'Ready-made models (use as model_id) and your own uploaded/generated models.',
50
+ input: {}, run: async (c) => { const [g, m] = await Promise.all([c.call('aistudio-global-model-list', { limit: 100 }), c.call('aistudio-model-list', { limit: 100 })]); return { ready_made: g.models || [], mine: m.models || m.items || [] } } }),
51
+ ]
@@ -0,0 +1,197 @@
1
+ // Film Studio + Image Studio: görsel, video, karakterli video, dudak senkronu, karakterler, Bible, Flow, Hikâyeden Film.
2
+ import { z, op, ID, IDS, NEG, n, ints, clean, opCost, quote } from './_util.js'
3
+ import { uploadAsset } from '../files.js'
4
+
5
+ const ASPECT = z.enum(['1:1', '16:9', '9:16', '3:4', '4:3'])
6
+ const VTIER = z.enum(['draft', 'standard', 'cinema', 'cinemapro'])
7
+ const CAMERA = z.enum(['crash_zoom_in', 'crash_zoom_out', 'dolly_in', 'dolly_out', 'orbit_left', 'orbit_right', 'crane_up', 'crane_down', 'pan_left', 'pan_right', 'fpv_drone', 'handheld'])
8
+
9
+ /** Rota süreleri (aistudio-list-routes). */
10
+ async function durations(c, jobType, tier) {
11
+ const r = await c.call('aistudio-list-routes').catch(() => null)
12
+ const row = (r?.routes || []).find((x) => x.job_type === jobType && x.quality_tier === tier)
13
+ const list = String(row?.allowed_durations || '').split(',').map(Number).filter(Boolean)
14
+ if (list.length) return list
15
+ return tier === 'cinema' || tier === 'cinemapro' ? [4, 6, 8] : [5, 10]
16
+ }
17
+ async function pickDuration(c, jobType, tier, d) {
18
+ const list = await durations(c, jobType, tier)
19
+ if (d == null) return list[0]
20
+ if (!list.includes(Number(d))) throw new Error(`duration must be one of ${list.join(', ')} seconds for ${jobType} / ${tier}`)
21
+ return Number(d)
22
+ }
23
+ const jobOut = (r) => clean({ job_id: String(r.job_id), status: r.status, tokens_charged: n(r.tokens_charged), video_job_id: r.child_job_id ? String(r.child_job_id) : undefined, approval: r.approval || undefined })
24
+
25
+ // ── görsel
26
+ const imageBody = (a) => {
27
+ const b = { job_type: 'text2image', prompt: a.prompt, quality_tier: a.tier || 'draft', aspect_ratio: a.aspect || '16:9' }
28
+ if (a.framing) b.framing = a.framing
29
+ if (a.character_ids?.length) b.character_ids = ints(a.character_ids)
30
+ if (a.reference_asset_ids?.length) b.reference_asset_ids = ints(a.reference_asset_ids)
31
+ if (a.edit_asset_id) { b.source_asset_id = Number(a.edit_asset_id); b.edit_mode = a.edit_mode || 'free' }
32
+ if (a.template) b.template = a.template
33
+ if (a.negative_prompt) b.negative_prompt = a.negative_prompt
34
+ return b
35
+ }
36
+ const imageRefs = (a) => (a.character_ids?.length || 0) + (a.reference_asset_ids?.length || 0)
37
+
38
+ export default [
39
+ op({ name: 'image_create', group: 'image', action: 'create', kind: 'spend', summary: 'Image Studio: generate or edit an image',
40
+ description: 'Generates an image from a prompt (Image Studio). Optional: your characters (consistent faces) and reference images (up to 4 in total), framing, or edit an existing image with one sentence (edit_asset_id + edit_mode). Costs tokens (draft is cheapest); failed jobs are refunded.',
41
+ positional: 'prompt',
42
+ input: { prompt: z.string().min(3).max(2000), tier: z.enum(['draft', 'standard', 'cinema']).optional().describe('draft (fast), standard, cinema (Basic/Pro plan)'), aspect: ASPECT.optional(),
43
+ framing: z.enum(['waist_up', 'waist_down', 'full_body']).optional(), character_ids: IDS('Your character ids (yesilcam_characters_list)', 4).optional(),
44
+ reference_asset_ids: IDS('Reference image asset ids', 4).optional(), edit_asset_id: ID('Image to edit (asset id)').optional(),
45
+ edit_mode: z.enum(['background', 'color', 'pose', 'free']).optional(), template: z.string().regex(/^[a-z0-9_-]{1,40}$/).optional().describe('Image template id (optional)'), negative_prompt: NEG },
46
+ quote: async (c, a) => { if (imageRefs(a) > 4) throw new Error('too_many_references: at most 4 characters + references'); const base = await opCost(c, `image_${a.tier || 'draft'}`); const ref = imageRefs(a) ? await opCost(c, 'image_ref') : null
47
+ return quote(c, [{ item: base.key, tokens: base.tokens }, ref && { item: `image_ref x${imageRefs(a)}`, tokens: ref.tokens * imageRefs(a) }], { balance: base.balance }) },
48
+ run: async (c, a) => jobOut(await c.call('aistudio-create-job', imageBody(a))),
49
+ wait: (r) => ({ kind: 'job', id: r.job_id }) }),
50
+
51
+ op({ name: 'video_create', group: 'video', action: 'create', kind: 'spend', summary: 'Film Studio: video from text, from an image, or restyle a video',
52
+ description: 'Creates a video. Text to video (prompt), image to video (image_asset_id, optional end frame), or restyle an existing 3-10 s video (restyle_video_asset_id, draft/standard). Optional camera preset and sound. Costs tokens per tier and duration; failed jobs are refunded.',
53
+ positional: 'prompt',
54
+ input: { prompt: z.string().max(2500).optional(), image_asset_id: ID('Start image asset id (image to video)').optional(), end_asset_id: ID('End frame image asset id').optional(),
55
+ restyle_video_asset_id: ID('Source video asset id (restyle)').optional(), reference_asset_ids: IDS('Reference images for restyle', 4).optional(),
56
+ tier: VTIER.optional().describe('draft, standard, cinema / cinemapro (Basic/Pro plan)'), aspect: ASPECT.optional(), duration: z.number().int().optional().describe('Seconds; allowed values depend on tier (usually 5 or 10)'),
57
+ camera: CAMERA.optional(), resolution: z.enum(['480p', '720p', '1080p']).optional(), sound: z.boolean().optional().describe('Generate audio (restyle)'), negative_prompt: NEG },
58
+ prepare: async (c, a) => {
59
+ const type = a.restyle_video_asset_id ? 'video2video' : (a.image_asset_id ? 'image2video' : 'text2video')
60
+ if (type !== 'image2video' && !a.prompt) throw new Error('prompt is required')
61
+ const tier = a.tier || 'draft'
62
+ const route = type === 'video2video' ? 'video2video' : type
63
+ return { type, tier, duration: await pickDuration(c, route, tier, a.duration) }
64
+ },
65
+ quote: async (c, a, p) => { const key = p.type === 'video2video' ? `v2v_${p.tier}_${p.duration}s` : `video_${p.tier}_${p.duration}s`; const x = await opCost(c, key); return quote(c, [{ item: key, tokens: x.tokens }], { balance: x.balance }) },
66
+ run: async (c, a, ctx, p) => {
67
+ const b = { job_type: p.type, prompt: a.prompt || '', quality_tier: p.tier, aspect_ratio: a.aspect || '16:9', duration: p.duration }
68
+ if (p.type === 'image2video') { b.source_asset_id = Number(a.image_asset_id); if (a.end_asset_id) b.end_asset_id = Number(a.end_asset_id) }
69
+ if (p.type === 'video2video') { b.source_asset_id = Number(a.restyle_video_asset_id); if (a.reference_asset_ids?.length) b.reference_image_ids = ints(a.reference_asset_ids); b.resolution = a.resolution || '720p'; if (a.sound) b.generate_audio = true }
70
+ if (a.camera) b.camera_preset = a.camera
71
+ if (a.negative_prompt) b.negative_prompt = a.negative_prompt
72
+ return jobOut(await c.call('aistudio-create-job', b))
73
+ },
74
+ wait: (r) => ({ kind: 'job', id: r.job_id }) }),
75
+
76
+ op({ name: 'character_video_create', group: 'video', action: 'characters', kind: 'spend', summary: 'Film Studio: video starring your saved characters',
77
+ description: 'Video with 1-4 of your saved characters keeping their faces. Optional first_frame_approval (draft/standard): the first frame is made first and the video waits for your approval (yesilcam_first_frame). Costs tokens; failed or rejected jobs are refunded.',
78
+ positional: 'prompt',
79
+ input: { prompt: z.string().min(3).max(2200), character_ids: IDS('Character ids (1-4)', 4).min(1), tier: z.enum(['draft', 'standard', 'premium']).optional(),
80
+ duration: z.number().int().optional(), aspect: ASPECT.optional(), first_frame_approval: z.boolean().optional(), negative_prompt: NEG },
81
+ prepare: async (c, a) => {
82
+ const tier = a.tier || 'draft'
83
+ const chain = Boolean(a.first_frame_approval)
84
+ if (chain && tier === 'premium') throw new Error('first_frame_approval works with draft or standard')
85
+ return { tier, chain, duration: await pickDuration(c, chain ? 'image2video' : 'character2video', tier, a.duration) }
86
+ },
87
+ quote: async (c, a, p) => {
88
+ if (p.chain) { const k = await opCost(c, 'keyframe'); const v = await opCost(c, `video_${p.tier}_${p.duration}s`); return quote(c, [{ item: 'keyframe', tokens: k.tokens }, { item: v.key, tokens: v.tokens }], { balance: v.balance }) }
89
+ const v = await opCost(c, `charvid_${p.tier}_${p.duration}s`); return quote(c, [{ item: v.key, tokens: v.tokens }], { balance: v.balance })
90
+ },
91
+ run: async (c, a, ctx, p) => jobOut(await c.call('aistudio-create-job', clean({ job_type: 'character2video', prompt: a.prompt, character_ids: ints(a.character_ids), quality_tier: p.tier, duration: p.duration, aspect_ratio: a.aspect || '16:9', first_frame_approval: p.chain ? 1 : undefined, negative_prompt: a.negative_prompt }))),
92
+ wait: (r) => ({ kind: 'job', id: r.video_job_id || r.job_id }) }),
93
+
94
+ op({ zeroRunsFree: true, name: 'first_frame', group: 'video', action: 'first-frame', kind: 'spend', summary: 'Approve, reject or redo the first frame of a character video',
95
+ description: 'For a character video created with first_frame_approval: approve starts the video, reject refunds the video cost, regen makes a new first frame (costs a keyframe). Unapproved frames expire after 24 h and are refunded.',
96
+ input: { video_job_id: ID('The video job id (video_job_id from character_video_create)'), action: z.enum(['approve', 'reject', 'regen']) },
97
+ quote: async (c, a) => { if (a.action !== 'regen') return quote(c, [{ item: a.action, tokens: 0 }]); const k = await opCost(c, 'keyframe'); return quote(c, [{ item: 'keyframe', tokens: k.tokens }], { balance: k.balance }) },
98
+ run: (c, a) => c.call('aistudio-create-job', { job_type: 'character2video', chain_action: a.action, video_job_id: Number(a.video_job_id) }),
99
+ wait: (r, a) => (a.action === 'reject' ? null : { kind: 'job', id: a.video_job_id }) }),
100
+
101
+ op({ name: 'lipsync_create', group: 'video', action: 'lipsync', kind: 'spend', summary: 'Lip sync a video to an audio file or to text in a chosen voice',
102
+ description: 'Makes the person in a video speak: give an audio asset, or text plus a voice id (yesilcam_voices_list). Tier standard or pro (pro needs your own audio). Costs tokens; failed jobs are refunded.',
103
+ input: { video_asset_id: ID('Source video asset id'), audio_asset_id: ID('Audio asset id').optional(), text: z.string().max(500).optional(), voice_id: z.string().optional().describe('Voice id from voices_list (with text)'), tier: z.enum(['standard', 'pro']).optional() },
104
+ prepare: (c, a) => {
105
+ if (!a.audio_asset_id && !(a.text && a.voice_id)) throw new Error('give audio_asset_id, or text + voice_id')
106
+ const tier = a.tier || 'standard'
107
+ if (tier === 'pro' && !a.audio_asset_id) throw new Error('pro tier needs audio_asset_id')
108
+ return { key: tier === 'pro' ? 'lipsync_pro' : (a.audio_asset_id ? 'lipsync_default' : 'lipsync_eleven'), tier }
109
+ },
110
+ quote: async (c, a, p) => { const x = await opCost(c, p.key); return quote(c, [{ item: p.key, tokens: x.tokens }], { balance: x.balance }) },
111
+ run: async (c, a, ctx, p) => jobOut(await c.call('aistudio-create-job', clean({ job_type: 'lipsync', quality_tier: p.tier, source_asset_id: Number(a.video_asset_id),
112
+ audio_asset_id: n(a.audio_asset_id), text: a.audio_asset_id ? undefined : a.text, tts_provider: a.audio_asset_id ? undefined : 'elevenlabs', elevenlabs_voice_id: a.audio_asset_id ? undefined : a.voice_id }))),
113
+ wait: (r) => ({ kind: 'job', id: r.job_id }) }),
114
+
115
+ op({ name: 'voices_list', group: 'voices', action: 'list', kind: 'read', summary: 'Voices for narration, lip sync and Story to Film',
116
+ description: 'Voice catalog for lip sync, Flow scene speech and the Story to Film narrator (ids, names, labels, preview links).', input: {},
117
+ run: async (c) => { const r = await c.call('aistudio-voice-list'); return { voices: (r.voices || []).map((v) => clean({ id: v.voice_id, name: v.name, labels: v.labels, preview: v.preview_url })) } } }),
118
+
119
+ // ── karakterler
120
+ op({ name: 'characters_list', group: 'characters', action: 'list', kind: 'read', summary: 'Your saved characters',
121
+ description: 'Saved characters (consistent faces across images and videos), including AI Influencer characters.', input: {},
122
+ run: async (c) => { const r = await c.call('aistudio-character-list'); return { characters: (r.characters || []).map((x) => clean({ id: String(x.id), name: x.name, thumb: x.thumb_url, bible: x.bible_status || undefined, signature: x.is_signature == 1 || undefined })) } } }),
123
+ op({ name: 'character_create', group: 'characters', action: 'create', kind: 'write', summary: 'Create a character from 1-4 photos',
124
+ description: 'Creates a reusable character from 1-4 images (asset ids, or local files that are uploaded first). Free.',
125
+ input: { name: z.string().min(1).max(80), image_asset_ids: IDS('Image asset ids', 4).optional(), image_paths: z.array(z.string()).max(4).optional().describe('Local image files to upload') },
126
+ positional: 'name',
127
+ run: async (c, a) => {
128
+ const ids = ints(a.image_asset_ids) || []
129
+ for (const p of a.image_paths || []) ids.push(Number((await uploadAsset(c, p, { kind: 'image' })).asset_id))
130
+ if (!ids.length) throw new Error('give image_asset_ids or image_paths')
131
+ return c.call('aistudio-character-create', { name: a.name, source_asset_ids: ids.slice(0, 4) })
132
+ } }),
133
+ op({ name: 'character_delete', group: 'characters', action: 'delete', kind: 'write', destructive: true, summary: 'Delete a character',
134
+ description: 'Deletes a saved character (generated files stay in the library).', input: { character_id: ID('Character id') }, positional: 'character_id',
135
+ run: (c, a) => c.call('aistudio-character-delete', { character_id: Number(a.character_id) }) }),
136
+ op({ name: 'bible_create', group: 'characters', action: 'bible', kind: 'spend', summary: 'Character Bible: a full character sheet from a concept',
137
+ description: 'Designs a new character from a short concept and builds its Bible: master image, turnaround, expressions, poses, outfit, props, environment and story notes (scope=full), or only the core set (scope=core). Costs tokens.',
138
+ input: { name: z.string().min(1).max(80), concept: z.string().min(10).max(1000), style: z.string().max(300).optional().describe('pixar_pastel, anime, realistic_cinematic, watercolor, comic, or free text'),
139
+ aspect: ASPECT.optional(), scope: z.enum(['core', 'full']).optional(), negative_prompt: NEG },
140
+ quote: async (c, a) => { const key = (a.scope || 'full') === 'core' ? 'character_bible_core' : 'character_bible'; const x = await opCost(c, key); return quote(c, [{ item: key, tokens: x.tokens }], { balance: x.balance }) },
141
+ run: (c, a) => c.call('aistudio-character-bible-create', clean({ name: a.name, concept: a.concept, style: a.style, aspect: a.aspect, scope: a.scope || 'full', negative_prompt: a.negative_prompt })),
142
+ wait: (r) => ({ kind: 'bible', id: r.character_id }) }),
143
+ op({ name: 'bible_get', group: 'characters', action: 'bible-get', kind: 'read', summary: 'Read a Character Bible',
144
+ description: 'Character Bible text and section images.', input: { character_id: ID('Character id') }, positional: 'character_id',
145
+ run: (c, a) => c.call('aistudio-character-bible-get', { character_id: Number(a.character_id) }) }),
146
+
147
+ op({ name: 'prompt_improve', group: 'prompt', action: 'improve', kind: 'spend', summary: 'Sam improves or shortens a prompt',
148
+ description: 'Sam rewrites a prompt for images (kind=image) or motion (kind=motion), or shortens a long video prompt (kind=shorten). Charged by actual usage (a small estimate first, the difference is refunded).',
149
+ positional: 'text',
150
+ input: { text: z.string().min(3).max(8000), kind: z.enum(['image', 'motion', 'shorten']).optional(), style: z.string().max(200).optional(), max_chars: z.number().int().min(200).max(2400).optional() },
151
+ quote: async (c) => { const r = await c.call('aistudio-motion-quote', { tier: 'draft', duration: 5 }); return quote(c, [{ item: 'assistant (estimate)', tokens: Number(r.assist || 0) }], { balance: n(r.balance), approximate: true, notes: 'Charged by actual usage; the difference is refunded.' }) },
152
+ run: (c, a) => c.call('aistudio-sam-improve', clean({ kind: a.kind || 'image', text: a.text, style: a.style, max_chars: a.max_chars })) }),
153
+
154
+ // ── Hikâyeden film ve Flow
155
+ op({ name: 'story_film_create', group: 'film', action: 'story', kind: 'spend', summary: 'Story to Film: turn a story into a cast, storyboard and film (Pro)',
156
+ description: 'Story to Film (Pro plan): the director reads your story, casts up to 5 characters and writes the storyboard. The upfront charge covers directing and casting; production is a second confirmed step (yesilcam_flow_produce) once the storyboard is ready. Styles: anime, 3d, watercolor, comic, cinematic, yesilcam, ink, pastel.',
157
+ positional: 'story',
158
+ input: { story: z.string().min(40).max(20000), title: z.string().max(200).optional(), style: z.enum(['anime', '3d', 'watercolor', 'comic', 'cinematic', 'yesilcam', 'ink', 'pastel']).optional(),
159
+ aspect: ASPECT.optional(), tier: z.enum(['draft', 'standard', 'cinema', 'cinemapro']).optional().describe('Scene video tier'), director: z.enum(['professional', 'premium']).optional(),
160
+ target_seconds: z.number().int().min(20).max(600).optional(), language: z.string().max(10).optional().describe('Narration language, e.g. en, tr (default auto)'),
161
+ narrator_voice_id: z.string().optional(), music: z.boolean().optional(), music_style: z.string().max(120).optional(), tone: z.string().max(60).optional(),
162
+ ink_transitions: z.enum(['off', 'auto', 'on']).optional().describe('Ink Landscape transitions between scenes'), negative_prompt: NEG },
163
+ prepare: (c, a) => ({ settings: clean({ style: a.style, aspect_ratio: a.aspect, default_tier: a.tier, director_tier: a.director, target_seconds: a.target_seconds, language: a.language, narrator_voice_id: a.narrator_voice_id, music_enabled: a.music, music_style: a.music_style, tone: a.tone, ink_transitions: a.ink_transitions }) }),
164
+ quote: async (c, a, p) => { const r = await c.call('aistudio-flow-story-quote', { story: a.story, settings: p.settings })
165
+ return quote(c, [{ item: 'directing', tokens: r.director_tokens }, { item: `casting (${r.cast_slots || 5} slots, unused refunded)`, tokens: r.cast_tokens }], { balance: n(r.balance), notes: `Production later (separate confirmation): about ${Number(r.produce_est_tokens || 0).toLocaleString('en-US')} tokens for ~${r.scenes_est} scenes.` }) },
166
+ run: (c, a, ctx, p) => c.call('aistudio-flow-story-create', clean({ title: a.title, story: a.story, settings: p.settings, negative_prompt: a.negative_prompt })),
167
+ wait: (r) => ({ kind: 'flow', id: r.flow_id }) }),
168
+ op({ name: 'flow_create', group: 'film', action: 'flow', kind: 'spend', summary: 'Flow: script to storyboard (Pro)',
169
+ description: 'Flow (Pro plan): creates a film project from your script and writes the storyboard (scenes you can edit). Then estimate and produce with yesilcam_flow_produce.',
170
+ input: { title: z.string().min(1).max(120), script: z.string().min(10).max(8000), aspect: ASPECT.optional(), tier: VTIER.optional(), music: z.boolean().optional(), music_prompt: z.string().max(500).optional(), negative_prompt: NEG },
171
+ quote: async (c) => { const x = await opCost(c, 'flow_storyboard'); return quote(c, [{ item: 'flow_storyboard', tokens: x.tokens }], { balance: x.balance }) },
172
+ run: async (c, a) => { const f = await c.call('aistudio-flow-create', { title: a.title, script_text: a.script, settings: clean({ aspect_ratio: a.aspect, default_tier: a.tier, music_enabled: a.music, music_prompt: a.music_prompt, negative_prompt: a.negative_prompt }) })
173
+ const s = await c.call('aistudio-flow-storyboard', { flow_id: f.flow_id }); return { flow_id: String(f.flow_id), ...s } },
174
+ wait: (r) => ({ kind: 'flow', id: r.flow_id }) }),
175
+ op({ name: 'flow_list', group: 'film', action: 'list', kind: 'read', summary: 'Your Flow and Story to Film projects',
176
+ description: 'Flow / Story to Film projects with status and final video.', input: { limit: z.number().int().min(1).max(100).optional() },
177
+ run: (c, a) => c.call('aistudio-flow-list', { limit: a.limit || 20 }) }),
178
+ op({ name: 'flow_get', group: 'film', action: 'get', kind: 'read', summary: 'A Flow project with its scenes',
179
+ description: 'Full Flow / Story to Film project: settings, scenes (prompt, characters, duration, tier, speech) and job states.', input: { flow_id: ID('Flow id') }, positional: 'flow_id',
180
+ run: (c, a) => c.call('aistudio-flow-get', { flow_id: Number(a.flow_id) }) }),
181
+ op({ name: 'flow_scene_update', group: 'film', action: 'scene', kind: 'write', summary: 'Edit a Flow scene before production',
182
+ description: 'Edits one scene: prompt, synopsis, characters, duration, tier, start/end frames, continuity, or speech (text + voice id; null removes speech).',
183
+ input: { scene_id: ID('Scene id'), prompt: z.string().max(2000).optional(), synopsis: z.string().max(500).optional(), character_ids: IDS('Characters', 4).optional(), duration: z.number().int().optional(), tier: VTIER.optional(),
184
+ start_asset_id: ID('Start frame (0 clears)').optional(), end_asset_id: ID('End frame (0 clears)').optional(), continuity: z.boolean().optional(), speech_text: z.string().max(500).optional(), speech_voice_id: z.string().optional(), remove_speech: z.boolean().optional() },
185
+ run: (c, a) => c.call('aistudio-flow-scene-update', clean({ scene_id: Number(a.scene_id), prompt: a.prompt, synopsis: a.synopsis, character_ids: ints(a.character_ids), duration: a.duration, quality_tier: a.tier,
186
+ start_asset_id: n(a.start_asset_id), end_asset_id: n(a.end_asset_id), continuity: a.continuity, speak: a.remove_speech ? null : (a.speech_text ? { mode: 'eleven', text: a.speech_text, elevenlabs_voice_id: a.speech_voice_id } : undefined) })) }),
187
+ op({ name: 'flow_produce', group: 'film', action: 'produce', kind: 'spend', summary: 'Produce the film of a ready Flow / Story to Film project',
188
+ description: 'Produces every scene, music and the final edit of a ready Flow project. The quote is the server estimate; nothing is charged if the balance is short.',
189
+ input: { flow_id: ID('Flow id'), negative_prompt: NEG }, positional: 'flow_id',
190
+ quote: async (c, a) => { const r = await c.call('aistudio-flow-estimate', { flow_id: Number(a.flow_id) }); return quote(c, [{ item: `${r.scene_count} scenes, ${r.total_duration} s`, tokens: r.estimated_cost }], { balance: n(r.balance) }) },
191
+ run: (c, a) => c.call('aistudio-flow-produce', clean({ flow_id: Number(a.flow_id), negative_prompt: a.negative_prompt })),
192
+ wait: (r, a) => ({ kind: 'flow', id: a.flow_id }) }),
193
+ op({ name: 'flow_control', group: 'film', action: 'control', kind: 'write', destructive: true, summary: 'Cancel or delete a Flow project',
194
+ description: 'cancel stops a running storyboard or production (unfinished parts are refunded); delete removes the project.',
195
+ input: { flow_id: ID('Flow id'), action: z.enum(['cancel', 'delete']) }, positional: 'flow_id',
196
+ run: (c, a) => c.call(a.action === 'cancel' ? 'aistudio-flow-cancel' : 'aistudio-flow-delete', { flow_id: Number(a.flow_id) }) }),
197
+ ]
@@ -0,0 +1,140 @@
1
+ // AI Influencer Studio: karakter (Sam ile tanımla / özellikler / kendi fotoğrafı), ses ve dil, sahne videosu (1–3 karakter),
2
+ // yeniden yarat (şablon / kendi videon), konuşan video, Keşfet + kopyala.
3
+ import { statSync } from 'node:fs'
4
+ import { z, op, ID, NEG, n, clean, opCost, quote } from './_util.js'
5
+ import { uploadInfluencerPhoto, uploadInfluencerVideo } from '../files.js'
6
+
7
+ const LANG6 = z.enum(['tr', 'en', 'de', 'fr', 'es', 'it'])
8
+ const subject = (a) => (a.influencer_id ? { influencer_id: Number(a.influencer_id) } : { character_id: Number(a.character_id) })
9
+ let cfgCache = null
10
+ async function cfg(c) { if (!cfgCache || cfgCache.at < Date.now() - 60_000) cfgCache = { at: Date.now(), v: await c.call('ys-influencer-config') }; return cfgCache.v }
11
+ async function price(c, key) { const p = (await cfg(c)).prices || {}; if (p[key] != null) return Number(p[key]); return (await opCost(c, key)).tokens }
12
+
13
+ // Yüklenen kaynak videoyu aynı süreçte yeniden yüklememek için önbellek (teklif → onaylı çalıştırma)
14
+ const videoCache = new Map()
15
+ async function sourceVideo(c, path, consent) {
16
+ const st = statSync(path); const k = `${path}:${st.size}:${st.mtimeMs}`
17
+ if (!videoCache.has(k)) videoCache.set(k, await uploadInfluencerVideo(c, path, consent))
18
+ return videoCache.get(k)
19
+ }
20
+ const speechOf = (a) => (a.speech_lines?.length ? { mode: a.speech_mode || 'voiceover', lines: a.speech_lines.map((l) => ({ cast: l.cast ?? 0, text: l.text })) } : undefined)
21
+
22
+ export default [
23
+ op({ name: 'influencer_config', group: 'influencer', action: 'config', kind: 'read', summary: 'AI Influencer prices, limits, speaking languages and templates',
24
+ description: 'AI Influencer Studio settings for this account: token prices per operation, plan, balance, limits (scene durations, cast size, speech length), speaking languages and recreate templates.', input: {},
25
+ run: async (c) => { const r = await cfg(c); return { ...r, templates: (r.templates || []).length } } }),
26
+ op({ name: 'influencer_define', group: 'influencer', action: 'define', kind: 'spend', summary: 'Describe your influencer to Sam in your own words',
27
+ description: 'Sam turns a free-text description (look, personality, style, wardrobe, favourite places) into a character definition you can review and pass to influencer_create as definition. Characters are always adults. Charged by actual usage (small).',
28
+ positional: 'text',
29
+ input: { text: z.string().min(10).max(4000), lang: LANG6.optional() },
30
+ quote: async (c) => { const r = await cfg(c); return quote(c, [{ item: 'Sam (estimate)', tokens: r.sam_estimate || 0 }], { balance: n(r.balance), approximate: true }) },
31
+ run: (c, a) => c.call('ys-influencer-sam-parse', { text: a.text, lang: a.lang || 'en' }) }),
32
+ op({ name: 'influencer_create', group: 'influencer', action: 'create', kind: 'spend', summary: 'Create an AI influencer (character set saved to Characters)',
33
+ description: 'Creates a consistent influencer: 4 views (portrait, full body, 3/4, profile) plus up to 3 outfits and 3 locations, saved to your Characters library. Define it with Sam (description), from a definition returned by influencer_define, or from your own photo (Basic/Pro; photo_consent required; a safety check rejects minors, public figures, several faces or nudity). Tier draft or standard.',
34
+ input: { name: z.string().min(1).max(60), description: z.string().min(10).max(4000).optional().describe('Free-text description (Sam defines it)'),
35
+ definition: z.object({ attributes: z.record(z.string(), z.any()).optional(), profile: z.record(z.string(), z.any()).optional() }).optional().describe('Output of influencer_define'),
36
+ attributes: z.record(z.string(), z.any()).optional().describe('Catalog attributes, e.g. {"character_type":"human_natural","gender":"female","age":"adult","style":"street"}'),
37
+ photo_path: z.string().optional().describe('Your own photo (JPG/PNG/WebP); Basic/Pro'), photo_consent: z.boolean().optional().describe('I am this person or have their permission'),
38
+ tier: z.enum(['draft', 'standard']).optional(), looks: z.number().int().min(0).max(3).optional(), locations: z.number().int().min(0).max(3).optional(), lang: LANG6.optional(), negative_prompt: NEG },
39
+ prepare: (c, a) => { if (a.photo_path && !a.photo_consent) throw new Error('consent_required'); return {} },
40
+ quote: async (c, a) => {
41
+ const prof = a.definition?.profile || {}
42
+ const r = await c.call('ys-influencer-quote', { tier: a.tier || 'draft', profile: prof, looks: a.looks ?? 2, locations: a.locations ?? 2, has_photo: a.photo_path ? 1 : 0 })
43
+ const sam = a.description && !a.definition ? (await cfg(c)).sam_estimate || 0 : 0
44
+ const images = a.definition ? r.images : 4 + (a.looks ?? 2) + (a.locations ?? 2)
45
+ return quote(c, [sam && { item: 'Sam definition (estimate)', tokens: sam }, { item: `images x${images}`, tokens: r.per_image * images }, r.safety && { item: 'photo safety check', tokens: r.safety }], { approximate: Boolean(sam) || !a.definition, notes: !a.definition ? 'Image count depends on the wardrobe and locations Sam defines (upper bound shown; you pay for the images actually made).' : undefined })
46
+ },
47
+ run: async (c, a) => {
48
+ let def = a.definition || {}
49
+ if (a.description && !a.definition) def = await c.call('ys-influencer-sam-parse', { text: a.description, lang: a.lang || 'en' })
50
+ let photo
51
+ if (a.photo_path) { const ph = await uploadInfluencerPhoto(c, a.photo_path, a.photo_consent); if (ph.status !== 'ok') return { rejected: true, reason: ph.reason || 'generic' }; photo = ph.media_id }
52
+ return c.call('ys-influencer-create', clean({ name: a.name, tier: a.tier || 'draft', attributes: { ...(def.attributes || {}), ...(a.attributes || {}) }, profile: def.profile, looks: a.looks ?? 2, locations: a.locations ?? 2, lang: a.lang || 'en', photo_media_id: photo, negative_prompt: a.negative_prompt }))
53
+ },
54
+ wait: (r) => (r.influencer_id ? { kind: 'influencer', id: r.influencer_id } : null) }),
55
+ op({ name: 'influencer_list', group: 'influencer', action: 'list', kind: 'read', summary: 'Your AI influencers', description: 'Your influencers with status and cover image.',
56
+ input: { limit: z.number().int().min(1).max(100).optional() },
57
+ run: async (c, a) => { const r = await c.call('ys-influencer-list', { limit: a.limit || 48 }); return { influencers: (r.influencers || []).map((i) => clean({ id: String(i.id), name: i.name, status: i.status, character_id: i.character_id ? String(i.character_id) : undefined, cover: i.cover_url, voice: i.profile?.voice?.name })) } } }),
58
+ op({ name: 'influencer_get', group: 'influencer', action: 'get', kind: 'read', summary: 'An influencer: profile, looks, locations, voice', description: 'Full influencer: attributes, profile (bio, tone, wardrobe, locations, palette, voice), looks and job state.',
59
+ input: { influencer_id: ID('Influencer id') }, positional: 'influencer_id', run: (c, a) => c.call('ys-influencer-get', { influencer_id: Number(a.influencer_id) }) }),
60
+ op({ name: 'influencer_update', group: 'influencer', action: 'update', kind: 'write', summary: 'Rename an influencer or edit its profile text', description: 'Changes the name or profile text (images do not change).',
61
+ input: { influencer_id: ID('Influencer id'), name: z.string().max(60).optional(), profile: z.record(z.string(), z.any()).optional() },
62
+ run: (c, a) => c.call('ys-influencer-update', clean({ influencer_id: Number(a.influencer_id), name: a.name, profile: a.profile })) }),
63
+ op({ name: 'influencer_delete', group: 'influencer', action: 'delete', kind: 'write', destructive: true, summary: 'Delete an influencer', description: 'Deletes the influencer, its character and private photo (generated videos stay in the library).',
64
+ input: { influencer_id: ID('Influencer id') }, positional: 'influencer_id', run: (c, a) => c.call('ys-influencer-delete', { influencer_id: Number(a.influencer_id) }) }),
65
+ op({ name: 'influencer_voices', group: 'influencer', action: 'voices', kind: 'read', summary: 'Voices for a speaking language (and your cloned voices)',
66
+ description: 'Voice catalog for one of 20 speaking languages, filtered by gender, age and tone, with previews; includes your own cloned voices.', input: { lang: z.string().min(2).max(5).describe('tr, en, de, fr, es, it, pt, nl, pl, sv, ro, el, ru, uk, ar, hi, id, ja, ko, zh') }, positional: 'lang',
67
+ run: (c, a) => c.call('ys-influencer-voices', { lang: a.lang }) }),
68
+ op({ name: 'influencer_voice_set', group: 'influencer', action: 'voice-set', kind: 'write', summary: 'Set the voice and speaking language of an influencer or character',
69
+ description: 'Saves the voice and language a character speaks in. Every talking, voice-over and lip-sync video uses this saved voice.',
70
+ input: { influencer_id: ID('Influencer id').optional(), character_id: ID('Character id').optional(), voice: z.string().describe('Voice id from influencer_voices'), lang: z.string().min(2).max(5) },
71
+ run: (c, a) => { if (!a.influencer_id && !a.character_id) throw new Error('give influencer_id or character_id'); return c.call('ys-influencer-voice-set', { ...subject(a), voice: a.voice, lang: a.lang }) } }),
72
+ op({ name: 'influencer_talk_assist', group: 'influencer', action: 'write', kind: 'spend', summary: 'Sam writes, translates or shortens a speech',
73
+ description: 'Sam writes a speech from a brief, translates a text, or shortens it to fit (max_chars). Charged by actual usage (minimum 100 tokens).',
74
+ input: { action: z.enum(['write', 'translate', 'shorten']), text: z.string().max(4000).optional(), brief: z.string().max(1000).optional(), lang: z.string().min(2).max(5), max_chars: z.number().int().min(40).max(600).optional() },
75
+ quote: async (c) => { const r = await cfg(c); return quote(c, [{ item: 'Sam text (estimate)', tokens: Math.max(100, Math.ceil(Number(r.sam_estimate || 0) * 0.4)) }], { balance: n(r.balance), approximate: true, notes: 'Charged up front and settled to actual use (minimum 100 tokens).' }) },
76
+ run: (c, a) => c.call('ys-influencer-talk-assist', clean({ action: a.action, text: a.text || '', lang: a.lang, brief: a.brief || '', max_chars: a.max_chars })) }),
77
+ op({ name: 'influencer_templates', group: 'influencer', action: 'templates', kind: 'read', summary: 'Recreate templates (videos you can redo with your character)',
78
+ description: 'Yeşilçam recreate templates: short videos you can recreate with your own influencer (id, category, title, duration, aspect, preview).', input: { lang: LANG6.optional() },
79
+ run: async (c, a) => { const r = await c.call('ys-influencer-templates'); const L = a.lang || 'en'; return { templates: (r.templates || []).filter((t) => t.is_active !== 0).map((t) => clean({ id: String(t.id), slug: t.slug, category: t.category, title: t.title?.[L] || t.title?.en, duration: n(t.duration), aspect: t.aspect, preview: t.video_url })) } } }),
80
+ op({ name: 'influencer_video', group: 'influencer', action: 'video', kind: 'spend', summary: 'AI Influencer video: scene (1-3 characters), recreate, or talking video',
81
+ description: 'mode=scene: a 5 or 10 s scene with 1-3 of your influencers/characters (roles, optional voice-over or lip-sync lines in their saved voices, products, a screenshot shown on a device screen). mode=recreate: redo a Yeşilçam template (template_id) or your own 3-30 s video (video_path or video_asset_id, Basic/Pro, video_consent required) with your character. mode=talk: the character speaks your text (up to 600 characters) in its saved voice and language.',
82
+ input: { mode: z.enum(['scene', 'recreate', 'talk']),
83
+ influencer_id: ID('Influencer id (single character)').optional(), character_id: ID('Character id (single character)').optional(),
84
+ cast: z.array(z.object({ influencer_id: z.union([z.string(), z.number()]).optional(), character_id: z.union([z.string(), z.number()]).optional(), role: z.string().max(160).optional() })).max(3).optional().describe('Scene cast: 1-3 characters'),
85
+ prompt: z.string().max(1800).optional().describe('Scene description (scene) or extra direction'), text: z.string().max(600).optional().describe('What the character says (talk)'),
86
+ tier: z.enum(['draft', 'standard']).optional(), duration: z.union([z.literal(5), z.literal(10)]).optional(), aspect: z.enum(['9:16', '1:1', '16:9']).optional(),
87
+ speech_mode: z.enum(['voiceover', 'lipsync']).optional(), speech_lines: z.array(z.object({ cast: z.number().int().min(0).max(2).optional(), text: z.string().max(600) })).max(6).optional(),
88
+ template_id: ID('Recreate template id').optional(), video_asset_id: ID('Your checked source video asset id').optional(), video_path: z.string().optional(), video_consent: z.boolean().optional(),
89
+ background: z.enum(['keep', 'location', 'style']).optional(), style: z.string().max(300).optional(), keep_sound: z.boolean().optional(),
90
+ product_asset_ids: z.array(z.union([z.string(), z.number()])).max(3).optional(), screen_asset_id: ID('Image shown on a phone/laptop screen').optional(), look_variant_id: z.string().optional(), location_index: z.number().int().min(0).max(3).optional(), negative_prompt: NEG },
91
+ prepare: async (c, a) => {
92
+ if (a.mode === 'scene' && !(a.prompt && a.prompt.length >= 3)) throw new Error('prompt is required for a scene')
93
+ if (a.mode === 'talk' && !(a.text && a.text.length >= 2)) throw new Error('text is required for a talking video')
94
+ if (a.mode !== 'scene' && !a.influencer_id && !a.character_id) throw new Error('give influencer_id or character_id')
95
+ if (a.mode === 'scene' && !a.cast?.length && !a.influencer_id && !a.character_id) throw new Error('give cast or influencer_id / character_id')
96
+ const p = {}
97
+ if (a.mode === 'recreate') {
98
+ if (!a.template_id && !a.video_asset_id && !a.video_path) throw new Error('give template_id, video_asset_id or video_path')
99
+ if (a.video_path) { if (!a.video_consent) throw new Error('consent_required'); const v = await sourceVideo(c, a.video_path, true); if (v.check?.status !== 'ok') throw new Error(`video_rejected_${v.check?.reason || 'generic'}`); p.asset_id = v.asset_id; p.seconds = Number(v.check.duration) }
100
+ else if (a.template_id) { const t = (await c.call('ys-influencer-templates')).templates || []; const tpl = t.find((x) => String(x.id) === String(a.template_id) || x.slug === a.template_id); if (!tpl) throw new Error('template not found'); p.template_id = tpl.id; p.seconds = Number(tpl.duration) || 10 }
101
+ else { p.asset_id = a.video_asset_id; p.seconds = 30 }
102
+ }
103
+ if (a.mode === 'talk') { const v = await c.call('ys-influencer-voice-get', subject(a)).catch(() => null); p.lang = v?.voice?.lang || 'en'; if (!v?.voice?.id) throw new Error('voice_not_set') }
104
+ return p
105
+ },
106
+ quote: async (c, a, p) => {
107
+ const tier = a.tier || 'draft'
108
+ if (a.mode === 'talk') { const r = await c.call('ys-influencer-quote', { kind: 'talk', text: a.text, lang: p.lang }); return quote(c, [{ item: `talking video ${r.seconds}s`, tokens: r.total }]) }
109
+ const speechChars = (a.speech_lines || []).reduce((s, l) => s + l.text.length, 0)
110
+ const speech = speechChars ? [{ item: 'speech', tokens: Math.ceil(speechChars / 100) * await price(c, 'inf_tts_100c') + (a.speech_mode === 'lipsync' ? (a.duration || 5) * await price(c, 'inf_lipsync_sec') : 0) }] : []
111
+ if (a.mode === 'recreate') { const r = await c.call('ys-influencer-quote', { kind: 'recreate', tier, seconds: p.seconds, uploaded: p.asset_id ? 1 : 0 }); return quote(c, [{ item: 'start frame', tokens: r.keyframe }, { item: `motion ${r.seconds}s`, tokens: r.per_second * r.seconds }, r.safety && p.asset_id && { item: 'safety check', tokens: r.safety }, ...speech], { approximate: Boolean(speech.length) || (!p.template_id && !a.video_path) }) }
112
+ const key = `charvid_${tier}_${a.duration || 5}s`
113
+ return quote(c, [{ item: key, tokens: (await opCost(c, key)).tokens }, ...speech], { approximate: Boolean(speech.length), notes: speech.length ? 'High-quality lip sync (one speaker) may be priced per second instead.' : undefined })
114
+ },
115
+ run: (c, a, ctx, p) => {
116
+ const tier = a.tier || 'draft'
117
+ const common = clean({ tier, negative_prompt: a.negative_prompt, look_variant_id: a.look_variant_id, screen_asset_id: n(a.screen_asset_id), speech: speechOf(a) })
118
+ if (a.mode === 'talk') return c.call('ys-influencer-video-create', clean({ mode: 'talk', ...subject(a), text: a.text, prompt: a.prompt, tier }))
119
+ if (a.mode === 'recreate') return c.call('ys-influencer-video-create', clean({ mode: 'recreate', ...subject(a), ...common, template_id: p.template_id, asset_id: p.asset_id ? Number(p.asset_id) : undefined, background: a.background, location_index: a.location_index, style: a.style, prompt: a.prompt, keep_sound: a.keep_sound, product_asset_ids: (a.product_asset_ids || []).map(Number) }))
120
+ const cast = a.cast?.length ? a.cast.map((m) => clean({ influencer_id: n(m.influencer_id), character_id: n(m.character_id), role: m.role })) : undefined
121
+ return c.call('ys-influencer-video-create', clean({ mode: 'scene', ...(cast ? { cast } : subject(a)), prompt: a.prompt, duration: a.duration || 5, aspect: a.aspect || '9:16', location_index: a.location_index, product_asset_ids: (a.product_asset_ids || []).map(Number), ...common }))
122
+ },
123
+ wait: (r) => ({ kind: 'influencer_job', id: r.job_id }) }),
124
+ op({ name: 'influencer_jobs', group: 'influencer', action: 'jobs', kind: 'read', summary: 'AI Influencer jobs (videos, character sets)', description: 'Recent AI Influencer jobs with status, progress and results.',
125
+ input: { limit: z.number().int().min(1).max(80).optional() }, run: (c, a) => c.call('ys-influencer-jobs', { limit: a.limit || 20 }) }),
126
+
127
+ // ── Keşfet
128
+ op({ name: 'discover', group: 'discover', action: 'feed', kind: 'read', summary: 'Discover: ready-made influencers you can copy, and community videos',
129
+ description: 'Browse ready-made influencer samples by niche (fashion, beauty, fitness, food, travel, tech, gaming, finance, business, home, music, books, art, sustainability, pets, learning, diy, comedy, nature, lifestyle), group (human, stylized, fantasy, animal) and format, or community videos (community=true).',
130
+ input: { niche: z.string().optional(), group: z.enum(['human', 'stylized', 'fantasy', 'animal']).optional(), format: z.enum(['talking', 'vertical', 'wide', 'square']).optional(), q: z.string().max(60).optional(), community: z.boolean().optional(), offset: z.number().int().min(0).optional(), limit: z.number().int().min(1).max(24).optional() },
131
+ run: async (c, a) => {
132
+ if (a.community) return c.call('ys-influencer-discover', clean({ op: 'community', offset: a.offset || 0, limit: a.limit || 12 }))
133
+ const r = await c.call('ys-influencer-discover', clean({ op: 'feed', niche: a.niche, group: a.group, format: a.format, q: a.q, offset: a.offset || 0, limit: a.limit || 12 }))
134
+ return { samples: (r.samples || []).map((s) => clean({ slug: s.slug, name: s.name, niche: s.niche, type: s.type, cover: s.cover_url, video: s.video_url, voice: s.voice?.name })), total: r.total, next: r.next, copy_price: r.adopt_price, already_copied: r.adopted }
135
+ } }),
136
+ op({ name: 'discover_copy', group: 'discover', action: 'copy', kind: 'spend', summary: 'Copy a ready-made influencer to your account',
137
+ description: 'Copies a Discover sample (by slug) into your influencers, ready to use. Copying the same sample again is free.', input: { slug: z.string().min(1), lang: LANG6.optional() }, positional: 'slug',
138
+ quote: async (c, a) => { const r = await c.call('ys-influencer-discover', { op: 'feed', q: a.slug, limit: 24 }); const owned = (r.adopted || []).includes(a.slug); return quote(c, [{ item: owned ? 'copy (already owned, free)' : 'inf_adopt', tokens: owned ? 0 : (r.adopt_price ?? await price(c, 'inf_adopt')) }]) },
139
+ run: (c, a) => c.call('ys-influencer-discover', clean({ op: 'adopt', slug: a.slug, lang: a.lang })) }),
140
+ ]
@@ -0,0 +1,56 @@
1
+ import { z, op, ID, n, clean } from './_util.js'
2
+ import { uploadAsset } from '../files.js'
3
+ import { checkOnce } from '../wait.js'
4
+
5
+ const assetView = (a) => clean({ id: String(a.id), kind: a.kind, origin: a.origin, label: a.label || a.share_title || undefined, url: a.public_url || a.spaces_url, thumb: a.thumb_url, width: n(a.width), height: n(a.height),
6
+ duration: n(a.duration_sec), created_at: n(a.created_at), visibility: a.is_published == 1 ? 'wall' : (a.is_public == 1 ? 'public' : (a.link_req_at || a.wall_req_at ? 'pending' : 'private')), prompt: a.prompt ? String(a.prompt).slice(0, 200) : undefined })
7
+
8
+ export default [
9
+ op({ name: 'jobs_list', group: 'jobs', action: 'list', kind: 'read', summary: 'Recent generation jobs (images, videos, try-ons)',
10
+ description: 'Recent Film Studio / Image Studio / Fashion jobs with status and result links. open_only lists jobs still running.',
11
+ input: { limit: z.number().int().min(1).max(100).optional(), offset: z.number().int().min(0).optional(), open_only: z.boolean().optional() },
12
+ run: async (c, a) => { const r = await c.call('aistudio-job-list', clean({ limit: a.limit || 20, offset: a.offset || 0, only_open: a.open_only ? 1 : undefined }));
13
+ return { jobs: (r.jobs || []).map((j) => clean({ id: String(j.id), type: j.job_type, status: j.status, tier: j.quality_tier, progress: n(j.render_progress), tokens_charged: n(j.tokens_charged), error: j.error_code, url: j.result_asset?.spaces_url, created_at: n(j.created_at) })) } } }),
14
+
15
+ op({ name: 'job_status', group: 'jobs', action: 'status', kind: 'read', summary: 'Status and result of a Film / Image / Fashion job',
16
+ description: 'Status, progress and result link of a generation job (use wait=true to wait for it).', input: { job_id: ID('Job id') }, positional: 'job_id',
17
+ run: (c, a) => checkOnce(c, 'job', a.job_id), wait: (r, a) => ({ kind: 'job', id: a.job_id }) }),
18
+ op({ name: 'library_list', group: 'library', action: 'list', kind: 'read', summary: 'Your library: images, videos and audio',
19
+ description: 'Lists files in the library (generated and uploaded) with ids and links. Your media is private: links are signed and expire after a few hours (download refreshes them). visibility shows private / pending (share request waiting for approval) / public link / wall.',
20
+ input: { kind: z.enum(['image', 'video']).optional(), limit: z.number().int().min(1).max(100).optional(), offset: z.number().int().min(0).optional() },
21
+ run: async (c, a) => { const r = await c.call('aistudio-asset-list', clean({ kind: a.kind, limit: a.limit || 40, offset: a.offset || 0 })); return { assets: (r.assets || []).map(assetView) } } }),
22
+ op({ name: 'library_upload', group: 'library', action: 'upload', kind: 'write', summary: 'Upload a local image, video or audio file to the library',
23
+ description: 'Uploads a local file (image up to 30 MB, video up to 500 MB, audio up to 50 MB) to the private library and returns its asset id for use in other tools. Free.',
24
+ input: { path: z.string().min(1).describe('Local file path'), label: z.string().max(80).optional(), kind: z.enum(['image', 'video', 'audio']).optional().describe('Detected from the file type when omitted') },
25
+ positional: 'path',
26
+ run: (c, a) => uploadAsset(c, a.path, { kind: a.kind, label: a.label }) }),
27
+ op({ name: 'library_rename', group: 'library', action: 'rename', kind: 'write', summary: 'Rename a library file',
28
+ description: 'Sets the label of a library file.', input: { asset_id: ID('Asset id'), label: z.string().min(1).max(120) },
29
+ positional: 'asset_id',
30
+ run: (c, a) => c.call('aistudio-rename-asset', { asset_id: Number(a.asset_id), label: a.label }) }),
31
+ op({ name: 'library_delete', group: 'library', action: 'delete', kind: 'write', destructive: true, summary: 'Delete a library file',
32
+ description: 'Deletes a file from the library.', input: { asset_id: ID('Asset id') }, positional: 'asset_id',
33
+ run: (c, a) => c.call('aistudio-delete-asset', { asset_id: Number(a.asset_id) }) }),
34
+ op({ name: 'share', group: 'library', action: 'share', kind: 'write', summary: 'Sharing: request a public link or The Wall, withdraw, make private, or check status',
35
+ description: 'Your media is private by default. action=request_link asks for a public link, request_wall asks to post it on The Wall (both may wait for approval), withdraw cancels requests, private removes the link and Wall listing, unwall removes it from The Wall, state shows the current sharing state.',
36
+ input: { asset_id: ID('Asset id'), action: z.enum(['state', 'request_link', 'request_wall', 'withdraw', 'private', 'unwall']), title: z.string().max(140).optional().describe('Title shown on The Wall') },
37
+ positional: 'asset_id',
38
+ run: (c, a) => {
39
+ const id = Number(a.asset_id)
40
+ switch (a.action) {
41
+ case 'state': return c.call('ys-share-state', { asset_id: id })
42
+ case 'request_link': return c.call('ys-share-request', clean({ asset_id: id, kind: 'link', title: a.title }))
43
+ case 'request_wall': return c.call('ys-share-request', clean({ asset_id: id, kind: 'wall', title: a.title }))
44
+ case 'withdraw': return c.call('ys-share-withdraw', { asset_id: id, kind: 'all' })
45
+ case 'private': return c.call('ys-share-private', { asset_id: id })
46
+ case 'unwall': return c.call('ys-share-unwall', { asset_id: id })
47
+ }
48
+ } }),
49
+ op({ name: 'share_notices', group: 'library', action: 'notices', kind: 'read', summary: 'Decisions on your share requests',
50
+ description: 'Unseen approval or rejection decisions for your public link / Wall requests.', input: {},
51
+ run: (c) => c.call('ys-share-notices') }),
52
+ op({ name: 'lineage', group: 'library', action: 'lineage', kind: 'read', summary: 'Where a file came from and what was made from it',
53
+ description: 'Shows parents and children of an asset, job, Assemble project, website or design (two generations).',
54
+ input: { kind: z.enum(['asset', 'job', 'assemble', 'website', 'design']), id: ID('Id') }, positional: ['kind', 'id'],
55
+ run: (c, a) => c.call('aistudio-lineage-get', { kind: a.kind, id: Number(a.id) }) }),
56
+ ]