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.
- package/README.md +30 -11
- package/package.json +1 -1
- package/src/cli.js +187 -95
- package/src/client.js +95 -25
- package/src/config.js +32 -10
- package/src/constants.js +4 -1
- package/src/files.js +87 -0
- package/src/mcp.js +88 -50
- package/src/ops/_util.js +43 -0
- package/src/ops/account.js +62 -0
- package/src/ops/animation.js +126 -0
- package/src/ops/cinema.js +58 -0
- package/src/ops/design.js +75 -0
- package/src/ops/fashion.js +51 -0
- package/src/ops/film.js +197 -0
- package/src/ops/influencer.js +140 -0
- package/src/ops/library.js +56 -0
- package/src/ops/longvideo.js +106 -0
- package/src/ops/maket.js +65 -0
- package/src/ops/social.js +26 -0
- package/src/ops/website.js +92 -0
- package/src/registry.js +38 -0
- package/src/run.js +36 -0
- package/src/wait.js +167 -0
- package/src/jobs.js +0 -45
package/src/files.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Dosya yükleme / indirme. Küçük dosyalar base64 (aistudio-upload-asset, ≤14 MB), büyükler imzalı PUT (ys-upload-init → PUT → ys-upload-complete).
|
|
2
|
+
// Kullanıcı medyası özeldir: indirme bağlantıları süreli imzalıdır; süresi geçmişse ys-media-resign ile yenilenir.
|
|
3
|
+
import { readFileSync, writeFileSync, statSync, mkdirSync, existsSync } from 'node:fs'
|
|
4
|
+
import { basename, extname, dirname, join, resolve } from 'node:path'
|
|
5
|
+
import { ApiError } from './client.js'
|
|
6
|
+
|
|
7
|
+
export const MIME = {
|
|
8
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.heic': 'image/heic',
|
|
9
|
+
'.mp4': 'video/mp4', '.mov': 'video/quicktime', '.webm': 'video/webm',
|
|
10
|
+
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.m4a': 'audio/mp4', '.ogg': 'audio/ogg',
|
|
11
|
+
'.json': 'application/json', '.dxf': 'application/dxf', '.svg': 'image/svg+xml', '.pdf': 'application/pdf',
|
|
12
|
+
}
|
|
13
|
+
export const mimeOf = (p) => MIME[extname(String(p)).toLowerCase()] || 'application/octet-stream'
|
|
14
|
+
export const kindOf = (mime) => (mime.startsWith('video') ? 'video' : mime.startsWith('audio') ? 'audio' : 'image')
|
|
15
|
+
const BASE64_MAX = 14 * 1024 * 1024
|
|
16
|
+
|
|
17
|
+
export function readLocal(path) {
|
|
18
|
+
const p = resolve(String(path))
|
|
19
|
+
if (!existsSync(p)) throw new ApiError('file_not_found', { path: p })
|
|
20
|
+
return { path: p, buf: readFileSync(p), size: statSync(p).size, mime: mimeOf(p), name: basename(p) }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function putPresigned(url, headers, buf, fetchImpl = globalThis.fetch) {
|
|
24
|
+
const r = await fetchImpl(url, { method: 'PUT', headers: headers || {}, body: buf })
|
|
25
|
+
if (!r.ok) throw new ApiError('upload_failed', { status: r.status })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Yerel dosya → kütüphane varlığı. → { asset_id, url, kind, thumb_url?, duration? } */
|
|
29
|
+
export async function uploadAsset(c, path, { kind, label } = {}, fetchImpl) {
|
|
30
|
+
const f = readLocal(path)
|
|
31
|
+
const k = kind || kindOf(f.mime)
|
|
32
|
+
if (f.size <= BASE64_MAX && /^(image|video|audio)\//.test(f.mime) && f.mime !== 'image/heic') {
|
|
33
|
+
const r = await c.call('aistudio-upload-asset', { file_base64: `data:${f.mime};base64,${f.buf.toString('base64')}`, kind: k, label: (label || f.name).slice(0, 80) })
|
|
34
|
+
return { asset_id: String(r.asset_id ?? r.id), url: r.spaces_url || r.url || null, kind: r.kind || k }
|
|
35
|
+
}
|
|
36
|
+
const init = await c.call('ys-upload-init', { kind: k, mime: f.mime, size: f.size, name: f.name })
|
|
37
|
+
await putPresigned(init.url, init.headers || { 'Content-Type': f.mime }, f.buf, fetchImpl)
|
|
38
|
+
const done = await c.call('ys-upload-complete', { key: init.key, kind: k, label: (label || f.name).slice(0, 80) })
|
|
39
|
+
return { asset_id: String(done.asset_id), url: done.url || null, kind: done.kind || k, thumb_url: done.thumb_url || null, duration: done.duration_sec ?? null }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** AI Influencer: kendi fotoğrafı (özel nesne) + açık onay + güvenlik denetimi. → { media_id, status, reason } */
|
|
43
|
+
export async function uploadInfluencerPhoto(c, path, consent, fetchImpl) {
|
|
44
|
+
if (!consent) throw new ApiError('consent_required')
|
|
45
|
+
const f = readLocal(path)
|
|
46
|
+
if (!/^image\/(jpeg|png|webp)$/.test(f.mime)) throw new ApiError('unsupported_type', { mime: f.mime })
|
|
47
|
+
const init = await c.call('ys-influencer-photo-init', { mime: f.mime, size: f.size })
|
|
48
|
+
await putPresigned(init.url, init.headers || { 'Content-Type': f.mime }, f.buf, fetchImpl)
|
|
49
|
+
return c.call('ys-influencer-photo-check', { key: init.key, consent: 1 })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** AI Influencer: kendi videosu (3–30 sn) → yükle + onay + denetim. → { asset_id, check } */
|
|
53
|
+
export async function uploadInfluencerVideo(c, path, consent, fetchImpl) {
|
|
54
|
+
if (!consent) throw new ApiError('consent_required')
|
|
55
|
+
const up = await uploadAsset(c, path, { kind: 'video' }, fetchImpl)
|
|
56
|
+
const check = await c.call('ys-influencer-video-check', { asset_id: Number(up.asset_id), consent: 1 })
|
|
57
|
+
return { asset_id: up.asset_id, check }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Ses örneği (ses klonu): presigned PUT. → key */
|
|
61
|
+
export async function uploadVoiceSample(c, path, fetchImpl) {
|
|
62
|
+
const f = readLocal(path)
|
|
63
|
+
const init = await c.call('ys-influencer-voice-sample-init', { filename: f.name, size: f.size })
|
|
64
|
+
await putPresigned(init.put_url, { 'Content-Type': init.content_type || f.mime }, f.buf, fetchImpl)
|
|
65
|
+
return init.key
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fileNameFromUrl = (url, fallback = 'download') => {
|
|
69
|
+
try { const b = basename(new URL(url).pathname); return b && b.includes('.') ? decodeURIComponent(b) : fallback } catch { return fallback }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** URL → dosya. `out` bir dizinse adı URL'den alınır. 403/400 (süresi dolmuş imza) → ys-media-resign ile bir kez yenilenir. */
|
|
73
|
+
export async function downloadUrl(c, url, out, fetchImpl = globalThis.fetch) {
|
|
74
|
+
if (!url) throw new ApiError('nothing_to_download')
|
|
75
|
+
let target = resolve(String(out || fileNameFromUrl(url)))
|
|
76
|
+
if (existsSync(target) && statSync(target).isDirectory()) target = join(target, fileNameFromUrl(url))
|
|
77
|
+
let res = await fetchImpl(url)
|
|
78
|
+
if ((res.status === 403 || res.status === 400) && c) {
|
|
79
|
+
const r = await c.call('ys-media-resign', { urls: [url] }).catch(() => null)
|
|
80
|
+
const fresh = r?.urls?.[0]
|
|
81
|
+
if (fresh) res = await fetchImpl(fresh)
|
|
82
|
+
}
|
|
83
|
+
if (!res.ok) throw new ApiError('download_failed', { status: res.status })
|
|
84
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
85
|
+
writeFileSync(target, Buffer.from(await res.arrayBuffer()))
|
|
86
|
+
return target
|
|
87
|
+
}
|
package/src/mcp.js
CHANGED
|
@@ -1,64 +1,102 @@
|
|
|
1
|
-
// Claude için MCP sunucusu
|
|
1
|
+
// Claude (ve diğer MCP istemcileri) için stdio MCP sunucusu. Araçlar işlem kataloğundan türetilir (CLI ile birebir).
|
|
2
|
+
// Jeton harcayan araçlar confirm=true olmadan YALNIZ teklif döner; istemci kullanıcı onayı olmadan jeton harcayamaz.
|
|
2
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
3
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
4
5
|
import { z } from 'zod'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { analyzePlanText } from './maket/analyze.js'
|
|
11
|
-
import { SITE_URL } from './constants.js'
|
|
6
|
+
import { makeClient, friendlyError, ApiError } from './client.js'
|
|
7
|
+
import { getCredentials } from './config.js'
|
|
8
|
+
import { selectOps, STUDIOS } from './registry.js'
|
|
9
|
+
import { execute } from './run.js'
|
|
10
|
+
import { VERSION, KEYS_URL } from './constants.js'
|
|
12
11
|
|
|
13
12
|
const text = (data) => ({ content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }] })
|
|
14
|
-
const fail = (e) => ({ isError: true, content: [{ type: 'text', text: `Error: ${e
|
|
15
|
-
const MIME = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.mp4': 'video/mp4', '.mp3': 'audio/mpeg', '.wav': 'audio/wav' }
|
|
13
|
+
const fail = (e) => ({ isError: true, content: [{ type: 'text', text: `Error: ${friendlyError(e)}` }] })
|
|
16
14
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
export const INSTRUCTIONS = `Yeşilçam Studios tools: AI film, image, animation, design, website, fashion try-on, Maket and AI Influencer studios on the user's own account and token wallet.
|
|
16
|
+
Rules:
|
|
17
|
+
- Tools that spend tokens return a QUOTE first and charge nothing. Show the price to the user and only after they agree call the same tool again with the same arguments plus confirm=true. Never set confirm=true on your own. Use max_tokens as a safety limit.
|
|
18
|
+
- Long jobs: pass wait=true (and save_to to download), or call yesilcam_wait with the returned kind and id. Progress is reported while waiting.
|
|
19
|
+
- Media is private; links are signed and expire after a few hours (yesilcam_download refreshes them). Sharing publicly goes through yesilcam_share (may need approval).
|
|
20
|
+
- Characters, influencers and people must be adults; real public figures are not allowed.
|
|
21
|
+
- Account, payment, API key and social-account connection settings are only in the web app (https://yesilcamstudios.com/app).`
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
server
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
23
|
+
export function buildToolSchema(op) {
|
|
24
|
+
const s = { ...op.input }
|
|
25
|
+
if (op.kind === 'spend') {
|
|
26
|
+
s.confirm = z.boolean().optional().describe('Set true ONLY after the user approved the quoted cost. Without it nothing is charged and a quote is returned.')
|
|
27
|
+
s.max_tokens = z.number().int().min(0).optional().describe('Refuse if the quote is above this many tokens')
|
|
28
|
+
}
|
|
29
|
+
if (op.wait) {
|
|
30
|
+
s.wait = z.boolean().optional().describe('Wait for the result (reports progress)')
|
|
31
|
+
s.max_wait_seconds = z.number().int().min(5).max(3600).optional().describe('How long to wait (default 900)')
|
|
32
|
+
s.save_to = z.string().optional().describe('Local file or folder to save the result to (implies wait)')
|
|
33
|
+
}
|
|
34
|
+
return s
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function toolDescription(op) {
|
|
38
|
+
const tag = op.kind === 'spend' ? (op.confirm ? ' [Needs confirm=true after the user approves.]' : ' [Spends tokens: returns a quote first; call again with confirm=true after the user approves.]') : op.kind === 'local' ? ' [Free, runs locally.]' : ''
|
|
39
|
+
return `${op.description}${tag}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createServer({ client: injected, studios, readOnly } = {}) {
|
|
43
|
+
const server = new McpServer({ name: 'yesilcam', version: VERSION }, { instructions: INSTRUCTIONS })
|
|
44
|
+
let cached = null
|
|
45
|
+
const client = () => {
|
|
46
|
+
if (injected) return injected
|
|
47
|
+
if (cached) return cached
|
|
48
|
+
const cred = getCredentials()
|
|
49
|
+
if (!cred) throw new ApiError('not_logged_in')
|
|
50
|
+
cached = makeClient(cred)
|
|
51
|
+
return cached
|
|
52
|
+
}
|
|
53
|
+
for (const op of selectOps({ studios, readOnly })) {
|
|
54
|
+
server.registerTool(`yesilcam_${op.name}`, {
|
|
55
|
+
title: op.summary,
|
|
56
|
+
description: toolDescription(op),
|
|
57
|
+
inputSchema: buildToolSchema(op),
|
|
58
|
+
annotations: { title: op.summary, readOnlyHint: op.kind === 'read' || op.kind === 'local', destructiveHint: Boolean(op.destructive), idempotentHint: op.kind === 'read' || op.kind === 'local', openWorldHint: op.kind !== 'local' },
|
|
59
|
+
}, async (args, extra) => {
|
|
60
|
+
try {
|
|
61
|
+
const token = extra?._meta?.progressToken
|
|
62
|
+
let tick = 0
|
|
63
|
+
const onProgress = token != null && extra?.sendNotification ? (st) => {
|
|
64
|
+
tick += 1
|
|
65
|
+
const pct = st?.progress != null ? Math.max(0, Math.min(100, Number(st.progress))) : null
|
|
66
|
+
extra.sendNotification({ method: 'notifications/progress', params: { progressToken: token, progress: pct ?? tick, ...(pct != null ? { total: 100 } : {}), message: [st?.status, st?.stage].filter(Boolean).join(' · ') } }).catch(() => {})
|
|
67
|
+
} : undefined
|
|
68
|
+
const r = await execute(op, op.kind === 'local' ? null : client(), args, { confirmed: args?.confirm === true, onProgress })
|
|
69
|
+
if (r.status === 'quote') return text({ status: 'quote_only_nothing_charged', quote: r.quote, next: `Show this price to the user. If they approve, call yesilcam_${op.name} again with the same arguments and confirm=true.` })
|
|
70
|
+
if (r.status !== 'done') return text(r)
|
|
71
|
+
return text({ ...(r.quote ? { quoted_tokens: r.quote.total } : {}), result: r.result, ...(r.final ? { final: r.final } : {}), ...(r.track ? { track: `yesilcam_wait kind=${r.track.kind} id=${r.track.id}` } : {}) })
|
|
72
|
+
} catch (e) { return fail(e) }
|
|
73
|
+
})
|
|
74
|
+
}
|
|
50
75
|
return server
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
export function parseArgs(argv) {
|
|
79
|
+
const o = { studios: [], readOnly: false }
|
|
80
|
+
for (let i = 0; i < argv.length; i++) {
|
|
81
|
+
const v = argv[i]
|
|
82
|
+
if (v === '--read-only') o.readOnly = true
|
|
83
|
+
else if (v === '--studios' && argv[i + 1]) o.studios = argv[++i].split(',').map((s) => s.trim()).filter(Boolean)
|
|
84
|
+
else if (v.startsWith('--studios=')) o.studios = v.slice(10).split(',').map((s) => s.trim()).filter(Boolean)
|
|
85
|
+
else if (v === '--help' || v === '-h') o.help = true
|
|
86
|
+
else if (v === '--version' || v === '-v') o.version = true
|
|
87
|
+
}
|
|
88
|
+
if (process.env.YESILCAM_MCP_STUDIOS && !o.studios.length) o.studios = process.env.YESILCAM_MCP_STUDIOS.split(',').map((s) => s.trim()).filter(Boolean)
|
|
89
|
+
if (process.env.YESILCAM_MCP_READ_ONLY === '1') o.readOnly = true
|
|
90
|
+
const bad = o.studios.filter((s) => !STUDIOS.includes(s))
|
|
91
|
+
if (bad.length) throw new Error(`unknown studio: ${bad.join(', ')} (use: ${STUDIOS.filter((s) => s !== 'core').join(', ')})`)
|
|
92
|
+
return o
|
|
59
93
|
}
|
|
60
94
|
|
|
61
|
-
export async function startStdio() {
|
|
62
|
-
const
|
|
95
|
+
export async function startStdio(argv = process.argv.slice(2)) {
|
|
96
|
+
const o = parseArgs(argv)
|
|
97
|
+
if (o.version) { process.stdout.write(`${VERSION}\n`); return }
|
|
98
|
+
if (o.help) { process.stdout.write(`yesilcam-mcp ${VERSION}: Yeşilçam Studios MCP server (stdio).\nOptions: --studios image,film,animation,cinema,design,website,fashion,maket,influencer --read-only\nSign in first with: yesilcam login (API key from ${KEYS_URL})\n`); return }
|
|
99
|
+
if (!getCredentials()) process.stderr.write(`yesilcam-mcp: not signed in. Run "yesilcam login" (API key from ${KEYS_URL}) or set YESILCAM_API_KEY.\n`)
|
|
100
|
+
const server = createServer(o)
|
|
63
101
|
await server.connect(new StdioServerTransport())
|
|
64
102
|
}
|
package/src/ops/_util.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// İşlem kataloğu yardımcıları. Her işlem:
|
|
2
|
+
// { name, group, action, summary, description, kind: 'read'|'write'|'spend'|'local', destructive?, input:{zod},
|
|
3
|
+
// positional?: alan adı (CLI ilk argüman), quote?(c,a) (spend), run(c,a,ctx), wait?: (sonuç, a) => { kind, id } }
|
|
4
|
+
// kind=spend: MCP'de confirm=true olmadan yalnız teklif döner (jeton harcanmaz); CLI'da --yes ya da etkileşimli onay.
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export { z }
|
|
8
|
+
export const ID = (d) => z.union([z.string(), z.number()]).describe(d)
|
|
9
|
+
export const IDS = (d, max = 10) => z.array(z.union([z.string(), z.number()])).max(max).describe(d)
|
|
10
|
+
export const NEG = z.string().max(500).optional().describe('Things to avoid (negative prompt), up to 500 characters')
|
|
11
|
+
export const ASPECT3 = z.enum(['16:9', '9:16', '1:1'])
|
|
12
|
+
export const n = (v) => (v == null || v === '' ? undefined : Number(v))
|
|
13
|
+
export const ints = (arr) => (Array.isArray(arr) ? arr.map((x) => Number(x)).filter((x) => Number.isFinite(x)) : undefined)
|
|
14
|
+
export const clean = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined && v !== null && v !== ''))
|
|
15
|
+
|
|
16
|
+
export const op = (o) => o
|
|
17
|
+
|
|
18
|
+
/** Tek işlem anahtarının bu plandaki bedeli (ai-studio modülü). */
|
|
19
|
+
export async function opCost(c, key) {
|
|
20
|
+
const r = await c.call('aistudio-operation-cost', { operation_key: key })
|
|
21
|
+
// Ücretli planda aylık ücretsiz hakkı kalan işlemler 0'a düşer (sunucu da ücret almaz)
|
|
22
|
+
const free = Number(r?.monthlyFreeRemaining || 0) > 0 || Number(r?.dailyFreeRemaining || 0) > 0 || r?.isFreeForAll === true
|
|
23
|
+
return { key: free ? `${key} (free monthly use, ${r.monthlyFreeRemaining ?? r.dailyFreeRemaining ?? ''} left)` : key, tokens: free ? 0 : Number(r?.effectiveCost ?? r?.effective_cost ?? 0), balance: r?.currentBalance != null ? Number(r.currentBalance) : null }
|
|
24
|
+
}
|
|
25
|
+
/** Başka modüller için (fashion-tryon, maket …). */
|
|
26
|
+
export async function moduleCost(c, moduleKey, operationKey) {
|
|
27
|
+
const r = await c.call('check-module-cost', { moduleKey, operationKey })
|
|
28
|
+
return { key: operationKey, tokens: Number(r?.effectiveCost ?? 0) }
|
|
29
|
+
}
|
|
30
|
+
export async function balanceOf(c) {
|
|
31
|
+
const r = await c.call('get-token-balance').catch(() => null)
|
|
32
|
+
return r && r.balance != null ? Number(r.balance) : null
|
|
33
|
+
}
|
|
34
|
+
/** Teklif nesnesi: { total, items:[{item, tokens}], balance, can_afford, notes? } */
|
|
35
|
+
export async function quote(c, items, { balance, notes, approximate } = {}) {
|
|
36
|
+
const list = items.filter(Boolean).map((i) => ({ item: i.item, tokens: Math.ceil(Number(i.tokens) || 0) }))
|
|
37
|
+
const total = list.reduce((s, i) => s + i.tokens, 0)
|
|
38
|
+
const bal = balance != null ? Number(balance) : await balanceOf(c)
|
|
39
|
+
return clean({ total, items: list, balance: bal, can_afford: bal == null ? undefined : bal >= total, approximate: approximate || undefined, notes })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** CLI/MCP'de dosya yolu gibi davranan alanlar (CLI ~ genişletmesi vb. için işaret). */
|
|
43
|
+
export const PATH = (d) => z.string().min(1).describe(d)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z, op, n, clean } from './_util.js'
|
|
2
|
+
import { checkOnce, waitFor, WAIT_KINDS, waitKindHelp } from '../wait.js'
|
|
3
|
+
import { downloadUrl } from '../files.js'
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
op({ name: 'account', group: 'account', action: 'show', kind: 'read', summary: 'Account, plan and token balance',
|
|
7
|
+
description: 'Shows the signed-in Yeşilçam Studios account: name, email, plan (Free, Basic, Pro), plan expiry, token balance and, when signed in with an API key, the key scopes.',
|
|
8
|
+
input: {},
|
|
9
|
+
run: async (c) => {
|
|
10
|
+
const [sub, bal, bill, ses] = await Promise.all([c.call('get-user-subscription-infos').catch(() => ({})), c.call('get-token-balance').catch(() => ({})), c.call('ys-billing-info').catch(() => null), c.session().catch(() => null)])
|
|
11
|
+
const plan = sub?.subscriptionPackageType || 'social-hive'
|
|
12
|
+
const label = { 'social-hive': 'Free', basic: 'Basic', professional: 'Pro' }
|
|
13
|
+
return clean({ name: [sub?.name, sub?.lastname].filter(Boolean).join(' ') || ses?.user?.name, email: sub?.email || ses?.user?.email, plan: label[bill?.effective_plan || plan] || plan,
|
|
14
|
+
plan_expires: (() => { const t = Number(sub?.subscriptionExpiredDate || bill?.expires_at); return t > 1e9 ? new Date(t * 1000).toISOString().slice(0, 10) : undefined })(), balance: bal?.balance != null ? Number(bal.balance) : undefined,
|
|
15
|
+
auth: c.kind, api_key: ses?.key ? { name: ses.key.name, prefix: ses.key.prefix, scopes: ses.scopes } : undefined })
|
|
16
|
+
} }),
|
|
17
|
+
op({ name: 'balance', group: 'account', action: 'balance', kind: 'read', summary: 'Token balance',
|
|
18
|
+
description: 'Current token balance of the account.', input: {},
|
|
19
|
+
run: async (c) => { const r = await c.call('get-token-balance'); return { balance: Number(r.balance) } } }),
|
|
20
|
+
op({ name: 'pricing', group: 'account', action: 'pricing', kind: 'read', summary: 'Token prices of every operation and token packs',
|
|
21
|
+
description: 'Lists base token prices per operation for all studios (your plan may apply a multiplier: Pro x0.8, Free x1.3; use the price tool for your exact price) and the token packs you can buy.',
|
|
22
|
+
input: { search: z.string().optional().describe('Filter operations by text, e.g. "video" or "inf_"') },
|
|
23
|
+
run: async (c, a) => {
|
|
24
|
+
const [ops, packs] = await Promise.all([c.call('get-all-module-pricing'), c.call('get-token-packages', { app: 'yesilcam' }).catch(() => [])])
|
|
25
|
+
const rows = (Array.isArray(ops) ? ops : []).filter((r) => ['ai-studio', 'fashion-tryon', 'maket'].includes(r.module_key) && String(r.status ?? '1') === '1')
|
|
26
|
+
.map((r) => ({ module: r.module_key, operation: r.operation_key, tokens: Number(r.base_cost_tokens) }))
|
|
27
|
+
.filter((r) => !a.search || `${r.module} ${r.operation}`.toLowerCase().includes(String(a.search).toLowerCase())).sort((x, y) => x.tokens - y.tokens)
|
|
28
|
+
const pk = (Array.isArray(packs) ? packs : packs?.items || []).map((p) => clean({ id: p.id, title: p.title, tokens: Number(p.token_amount), bonus_percent: Number(p.bonus_percentage || 0), usd: n(p.price_usd), usd_without_plan: n(p.price_usd_standard), try: n(p.price_tr), try_without_plan: n(p.price_tr_standard) }))
|
|
29
|
+
return { operations: rows, packs: pk, note: 'Base prices. Your plan multiplier applies (Pro x0.8, Basic x1.0, Free x1.3). Failed jobs are refunded.' }
|
|
30
|
+
} }),
|
|
31
|
+
op({ name: 'price', group: 'account', action: 'price', kind: 'read', summary: 'Exact token price of one operation for your plan',
|
|
32
|
+
description: 'Exact token price of one operation key for the signed-in plan (keys come from the pricing tool, e.g. image_standard, video_draft_5s, inf_talk_veo_sec).',
|
|
33
|
+
input: { operation: z.string().min(2).describe('Operation key, e.g. image_standard'), module: z.enum(['ai-studio', 'fashion-tryon', 'maket']).optional().describe('Module (default ai-studio)') },
|
|
34
|
+
positional: 'operation',
|
|
35
|
+
run: async (c, a) => { const r = await c.call('check-module-cost', { moduleKey: a.module || 'ai-studio', operationKey: a.operation }); return clean({ operation: a.operation, tokens: n(r.effectiveCost), base_tokens: n(r.baseCost), multiplier: n(r.multiplier), balance: n(r.currentBalance) }) } }),
|
|
36
|
+
op({ name: 'transactions', group: 'account', action: 'transactions', kind: 'read', summary: 'Recent token transactions',
|
|
37
|
+
description: 'Recent token transactions (charges, refunds, grants, purchases).',
|
|
38
|
+
input: { page: z.number().int().min(1).optional(), limit: z.number().int().min(1).max(100).optional() },
|
|
39
|
+
run: (c, a) => c.call('get-token-transactions', { page: a.page || 1, limit: a.limit || 20 }) }),
|
|
40
|
+
|
|
41
|
+
// ── ortak bekleme / durum / indirme
|
|
42
|
+
op({ name: 'status', group: 'wait', action: 'status', kind: 'read', summary: 'Status of any job or project',
|
|
43
|
+
description: `One status read of a running job or project. kind: ${waitKindHelp()}.`,
|
|
44
|
+
input: { kind: z.enum(WAIT_KINDS).describe('What to check'), id: z.union([z.string(), z.number()]).describe('Job / project id (renders: projectId:renderId)') },
|
|
45
|
+
positional: ['kind', 'id'],
|
|
46
|
+
run: (c, a) => checkOnce(c, a.kind, a.id) }),
|
|
47
|
+
op({ name: 'wait', group: 'wait', action: 'until', kind: 'read', summary: 'Wait until a job or project finishes (optionally download the result)',
|
|
48
|
+
description: `Polls a job or project until it finishes, needs your action, or max_wait_seconds passes; reports progress. Optionally saves the result file. kind: ${waitKindHelp()}.`,
|
|
49
|
+
input: { kind: z.enum(WAIT_KINDS), id: z.union([z.string(), z.number()]), max_wait_seconds: z.number().int().min(5).max(3600).optional().describe('Default 600'),
|
|
50
|
+
save_to: z.string().optional().describe('Local file or folder to save the result to') },
|
|
51
|
+
positional: ['kind', 'id'],
|
|
52
|
+
run: async (c, a, ctx) => {
|
|
53
|
+
const st = await waitFor(c, a.kind, a.id, { timeoutMs: (a.max_wait_seconds || 600) * 1000, onTick: ctx?.onProgress })
|
|
54
|
+
if (a.save_to && st.ok && st.url) st.saved = await downloadUrl(c, st.url, a.save_to)
|
|
55
|
+
return st
|
|
56
|
+
} }),
|
|
57
|
+
op({ name: 'download', group: 'library', action: 'download', kind: 'read', summary: 'Download a result or library file',
|
|
58
|
+
description: 'Downloads a file from a Yeşilçam Studios URL (private signed links are refreshed automatically) to a local path.',
|
|
59
|
+
input: { url: z.string().url().describe('File URL from a result or the library'), save_to: z.string().optional().describe('Local file or folder (default: current folder)') },
|
|
60
|
+
positional: 'url',
|
|
61
|
+
run: async (c, a) => ({ saved: await downloadUrl(c, a.url, a.save_to || '.') }) }),
|
|
62
|
+
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Animation Studio: 25 tarz (tek motor ailesi aistudio-anim-*), Inkmation (aistudio-inkmation-*), Custom (aistudio-motion-*).
|
|
2
|
+
// Ink Landscape = engine "inkland" (palet + mevsim).
|
|
3
|
+
import { z, op, ID, NEG, ASPECT3, n, clean, quote } from './_util.js'
|
|
4
|
+
import { uploadAsset } from '../files.js'
|
|
5
|
+
|
|
6
|
+
// Uygulama vitrinindeki ad + sunucu motoru (+ varyant). Ad çevrilmez.
|
|
7
|
+
export const STYLES = [
|
|
8
|
+
['foliomation', 'Foliomation'], ['papercraft', 'Papercraft'], ['dioramation', 'Dioramation'], ['typemation', 'Typemation'], ['fluxmation', 'Fluxmation'],
|
|
9
|
+
['puppetmation', 'Puppetmation'], ['inkland', 'Ink Landscape'], ['popbook', 'Popbook (interactive book)'], ['labmation', 'Labmation (interactive lab)'],
|
|
10
|
+
['wuxiamation-legend2d', 'Wuxiamation · Legend 2D'], ['wuxiamation-cinematic3d', 'Wuxiamation · Cinematic 3D'], ['celmation', 'Celmation'], ['neonmation', 'Neonmation'],
|
|
11
|
+
['toymation', 'Toymation'], ['toonmation', 'Toonmation'], ['shadowmation', 'Shadowmation'], ['pixelmation', 'Pixelmation'], ['panelmation', 'Panelmation'],
|
|
12
|
+
['claymation', 'Claymation'], ['knitmation', 'Knitmation'], ['sumimation', 'Sumimation'], ['postermation', 'Postermation'], ['epicmation', 'Epicmation'],
|
|
13
|
+
]
|
|
14
|
+
const STYLE_IDS = STYLES.map((s) => s[0])
|
|
15
|
+
const MOTION = ['wuxiamation', 'celmation', 'neonmation', 'toymation', 'toonmation', 'pixelmation', 'panelmation', 'claymation', 'knitmation', 'sumimation', 'postermation', 'epicmation']
|
|
16
|
+
const engineOf = (style) => { const [e, v] = String(style).split('-'); return { engine: e, variant: v } }
|
|
17
|
+
|
|
18
|
+
const animInput = {
|
|
19
|
+
style: z.enum(STYLE_IDS).describe('Animation style (see yesilcam_animation_styles)'),
|
|
20
|
+
prompt: z.string().min(12).max(8000).describe('What the film is about (story, topic or script)'),
|
|
21
|
+
title: z.string().max(200).optional(),
|
|
22
|
+
lang: z.enum(['tr', 'en', 'de', 'fr', 'es', 'it']).optional().describe('Narration language'),
|
|
23
|
+
aspect: ASPECT3.optional(), seconds: z.number().int().min(10).max(120).optional().describe('Target length (default 30)'),
|
|
24
|
+
voice: z.string().optional().describe('Voice id (yesilcam_animation_voices); empty = automatic'),
|
|
25
|
+
music: z.string().max(120).optional().describe('auto, none, or a music description'),
|
|
26
|
+
captions: z.enum(['bottom', 'top', 'none']).optional(), narrate: z.boolean().optional(),
|
|
27
|
+
palette: z.string().optional().describe('postermation: auto|bosphorus|riviera|noir|pastel · inkland: auto|ink|sepia|qinglu|night|yesilcam'),
|
|
28
|
+
season: z.enum(['auto', 'spring', 'summer', 'autumn', 'winter']).optional().describe('Ink Landscape only'),
|
|
29
|
+
note: z.string().max(300).optional().describe('Extra style note'),
|
|
30
|
+
negative_prompt: NEG,
|
|
31
|
+
}
|
|
32
|
+
const animOptions = (a) => { const { variant } = engineOf(a.style); return clean({ lang: a.lang, aspect: a.aspect, maxSeconds: a.seconds, voice: a.voice, music: a.music, captions: a.captions, narrate: a.narrate, palette: a.palette, season: a.season, style: a.note, variant, negative_prompt: a.negative_prompt }) }
|
|
33
|
+
const TIERS = z.enum(['premium', 'professional', 'cinema'])
|
|
34
|
+
|
|
35
|
+
const renderInput = { project_id: ID('Animation project id'), format: z.enum(['mp4', 'webp', 'gif']).optional(), quality: z.enum(['hd', '4k']).optional().describe('mp4 only'), aspect: ASPECT3.optional() }
|
|
36
|
+
const renderBody = (a) => clean({ project_id: Number(a.project_id), format: a.format || 'mp4', quality: (a.format || 'mp4') === 'mp4' ? (a.quality || 'hd') : undefined, aspect: a.aspect })
|
|
37
|
+
|
|
38
|
+
export default [
|
|
39
|
+
op({ name: 'animation_styles', group: 'animation', action: 'styles', kind: 'local', summary: 'The 25 animation styles',
|
|
40
|
+
description: 'Lists Animation Studio styles: 23 engine styles (including Ink Landscape and two Wuxiamation variants), plus Inkmation (yesilcam_inkmation_create) and Custom (yesilcam_custom_animation_create). "cinema" tier is available for moving styles on Basic/Pro.',
|
|
41
|
+
input: {},
|
|
42
|
+
run: () => ({ styles: STYLES.map(([id, name]) => ({ id, name, cinema_tier: MOTION.includes(engineOf(id).engine), interactive: ['popbook', 'labmation'].includes(id) || undefined })), also: [{ id: 'inkmation', name: 'Inkmation', tool: 'inkmation_create' }, { id: 'custom', name: 'Custom', tool: 'custom_animation_create' }] }) }),
|
|
43
|
+
op({ name: 'animation_voices', group: 'animation', action: 'voices', kind: 'read', summary: 'Narration voices for Animation and Inkmation',
|
|
44
|
+
description: 'Voices for Animation Studio and Inkmation narration (id, name, language, gender, preview).', input: {},
|
|
45
|
+
run: (c) => c.call('aistudio-inkmation-voices') }),
|
|
46
|
+
op({ name: 'animation_create', group: 'animation', action: 'create', kind: 'spend', summary: 'Animation Studio: make an animated film in one of 25 styles',
|
|
47
|
+
description: 'Creates an animated film in a chosen style: the director writes scenes, images, narration and music are made, then you render MP4/WebP/GIF (yesilcam_animation_render). Tiers: premium (best story craft), professional (fast, economical), cinema (moving styles, Basic/Pro). The estimate is charged up front and unused tokens are refunded at the end.',
|
|
48
|
+
positional: 'prompt',
|
|
49
|
+
input: { ...animInput, tier: TIERS.optional().describe('premium (default), professional, cinema') },
|
|
50
|
+
quote: async (c, a) => { const { engine } = engineOf(a.style); const r = await c.call('aistudio-anim-quote', { engine, prompt: a.prompt, options: animOptions(a) })
|
|
51
|
+
const t = r.tiers?.[a.tier || 'premium']; if (!t) throw new Error(`tier ${a.tier} is not available for ${a.style}`)
|
|
52
|
+
return quote(c, Object.entries(t.parts || { total: t.tokens }).map(([k, v]) => ({ item: k, tokens: v })), { balance: n(r.balance), notes: 'Estimate charged up front; unused tokens are refunded. Rendering is a separate small charge.' }) },
|
|
53
|
+
run: async (c, a) => { const { engine } = engineOf(a.style); return c.call('aistudio-anim-create', clean({ engine, tier: a.tier || 'premium', prompt: a.prompt, title: a.title, options: animOptions(a) })) },
|
|
54
|
+
wait: (r) => ({ kind: 'anim', id: r.project_id }) }),
|
|
55
|
+
op({ name: 'animation_story', group: 'animation', action: 'story', kind: 'spend', summary: 'Sam writes or improves an animation story',
|
|
56
|
+
description: 'Sam writes (mode=write) or improves (mode=improve) the story/prompt for an animation style or Inkmation. Charged by actual usage.',
|
|
57
|
+
input: { style: z.enum([...STYLE_IDS, 'inkmation']), text: z.string().max(6000), mode: z.enum(['write', 'improve']).optional(), lang: z.enum(['tr', 'en', 'de', 'fr', 'es', 'it']).optional(), seconds: z.number().int().min(10).max(300).optional() },
|
|
58
|
+
quote: async (c, a) => { const { engine } = engineOf(a.style); const r = await c.call('aistudio-anim-quote', { engine: engine === 'inkmation' ? 'foliomation' : engine, prompt: a.text || 'story', options: clean({ lang: a.lang, maxSeconds: a.seconds }) }).catch(() => ({}))
|
|
59
|
+
return quote(c, [{ item: 'assistant (estimate)', tokens: Number(r.assist || 0) }], { balance: n(r.balance), approximate: true }) },
|
|
60
|
+
run: (c, a) => { const { engine, variant } = engineOf(a.style); return c.call('aistudio-anim-story-assist', { engine, mode: a.mode || 'write', text: a.text || '', options: clean({ lang: a.lang, maxSeconds: a.seconds, variant }) }) } }),
|
|
61
|
+
op({ name: 'animation_list', group: 'animation', action: 'list', kind: 'read', summary: 'Your animation projects',
|
|
62
|
+
description: 'Animation Studio projects (newest first).', input: { style: z.string().optional() },
|
|
63
|
+
run: (c, a) => c.call('aistudio-anim-list', clean({ engine: a.style ? engineOf(a.style).engine : undefined })) }),
|
|
64
|
+
op({ name: 'animation_get', group: 'animation', action: 'get', kind: 'read', summary: 'An animation project with its renders',
|
|
65
|
+
description: 'Status, progress, scenes and renders of an animation project.', input: { project_id: ID('Project id') }, positional: 'project_id',
|
|
66
|
+
run: async (c, a) => { const r = await c.call('aistudio-anim-get', { project_id: Number(a.project_id) }); if (r?.spec) r.spec = { scenes: (r.spec.scenes || []).length }; return r } }),
|
|
67
|
+
op({ name: 'animation_render', group: 'animation', action: 'render', kind: 'spend', summary: 'Render an animation to MP4 (HD/4K), WebP or GIF',
|
|
68
|
+
description: 'Renders a ready animation project to a video file. Priced by render time (small); settled after rendering.',
|
|
69
|
+
input: renderInput, positional: 'project_id',
|
|
70
|
+
quote: async (c, a) => { const r = await c.call('aistudio-anim-render-quote', renderBody(a)); return quote(c, [{ item: `render ${r.format} ${r.quality || ''} ${r.duration || ''}s`, tokens: r.tokens }], { balance: n(r.balance) }) },
|
|
71
|
+
run: async (c, a) => { const r = await c.call('aistudio-anim-render', renderBody(a)); return { render_id: String(r.render_id), status: r.status, tokens_charged: n(r.tokens_charged), wait_id: `${a.project_id}:${r.render_id}` } },
|
|
72
|
+
wait: (r) => ({ kind: 'anim_render', id: r.wait_id }) }),
|
|
73
|
+
op({ name: 'animation_share', group: 'animation', action: 'share', kind: 'write', summary: 'Make an animation public (share link) or private',
|
|
74
|
+
description: 'Creates or removes a public share link for a ready animation project.', input: { project_id: ID('Project id'), public: z.boolean().optional().describe('default true') }, positional: 'project_id',
|
|
75
|
+
run: async (c, a) => { const r = await c.call('aistudio-anim-share', { project_id: Number(a.project_id), public: a.public !== false }); return { ...r, url: r.share_token ? `https://yesilcamstudios.com/app/p/${r.share_token}` : null } } }),
|
|
76
|
+
|
|
77
|
+
// ── Custom
|
|
78
|
+
op({ name: 'custom_animation_create', group: 'animation', action: 'custom', kind: 'spend', summary: 'Animation › Custom: animate your own image along a camera path',
|
|
79
|
+
description: 'Turns an image into a 5, 10 or 15 second video. Give a motion prompt and/or camera moves (each segment at least 3 s; segments must add up to the duration) and up to 3 objects to move. Tier draft (720p) or standard (1080p). Priced per second.',
|
|
80
|
+
input: { image_asset_id: ID('Image asset id').optional(), image_path: z.string().optional().describe('Local image (uploaded first)'), prompt: z.string().max(1500).optional(),
|
|
81
|
+
tier: z.enum(['draft', 'standard']).optional(), aspect: ASPECT3.optional(), duration: z.union([z.literal(5), z.literal(10), z.literal(15)]).optional(),
|
|
82
|
+
camera: z.array(z.object({ axis: z.enum(['pan', 'tilt', 'zoom', 'roll', 'horizontal', 'vertical']), value: z.number().min(-10).max(10), seconds: z.number().int().min(3) })).max(3).optional().describe('Guided camera segments'),
|
|
83
|
+
objects: z.array(z.object({ x0: z.number(), y0: z.number(), x1: z.number(), y1: z.number(), label: z.string().max(80) })).max(3).optional().describe('Objects to move (0..1 coordinates from start to end)'), negative_prompt: NEG },
|
|
84
|
+
prepare: (c, a) => {
|
|
85
|
+
const duration = a.duration || 5
|
|
86
|
+
if (a.camera?.length && a.camera.reduce((s, x) => s + x.seconds, 0) !== duration) throw new Error(`camera segment seconds must add up to ${duration}`)
|
|
87
|
+
if (!a.prompt && !a.camera?.length && !a.objects?.length) throw new Error('give a prompt, camera segments or objects')
|
|
88
|
+
if (!a.image_asset_id && !a.image_path) throw new Error('give image_asset_id or image_path')
|
|
89
|
+
return { duration, tier: a.tier || 'standard' }
|
|
90
|
+
},
|
|
91
|
+
quote: async (c, a, p) => { const r = await c.call('aistudio-motion-quote', { tier: p.tier, duration: p.duration }); return quote(c, [{ item: `${p.tier} ${p.duration}s`, tokens: r.price?.total }], { balance: n(r.balance) }) },
|
|
92
|
+
run: async (c, a, ctx, p) => {
|
|
93
|
+
const src = a.image_asset_id ? Number(a.image_asset_id) : Number((await uploadAsset(c, a.image_path, { kind: 'image' })).asset_id)
|
|
94
|
+
const camera = a.camera?.length ? { mode: 'guided', style: 'pan', segments: a.camera.map((s) => ({ axis: s.axis, value: s.value, seconds: s.seconds })) } : undefined
|
|
95
|
+
return c.call('aistudio-motion-create', clean({ source_asset_id: src, prompt: a.prompt, tier: p.tier, aspect: a.aspect, duration: p.duration, camera, objects: a.objects, negative_prompt: a.negative_prompt }))
|
|
96
|
+
},
|
|
97
|
+
wait: (r) => ({ kind: 'motion', id: r.job_id }) }),
|
|
98
|
+
op({ name: 'custom_animation_list', group: 'animation', action: 'custom-list', kind: 'read', summary: 'Your Custom animations',
|
|
99
|
+
description: 'Animation › Custom jobs with status and result video.', input: { limit: z.number().int().min(1).max(48).optional() },
|
|
100
|
+
run: (c, a) => c.call('aistudio-motion-list', { limit: a.limit || 24 }) }),
|
|
101
|
+
|
|
102
|
+
// ── Inkmation
|
|
103
|
+
op({ name: 'inkmation_create', group: 'inkmation', action: 'create', kind: 'spend', summary: 'Inkmation: brush-drawn explainer film from a story',
|
|
104
|
+
description: 'Inkmation draws your story with an animated brush, with narration and music in Turkish and/or English. Pick a platform (sets aspect and max length) and a drawing style. Tiers premium / professional. Then render with yesilcam_inkmation_render.',
|
|
105
|
+
positional: 'story',
|
|
106
|
+
input: { story: z.string().min(40).max(12000), title: z.string().max(200).optional(), tier: z.enum(['premium', 'professional']).optional(),
|
|
107
|
+
platform: z.enum(['youtube', 'youtube-shorts', 'instagram-reels', 'tiktok', 'story', 'instagram-post', 'instagram-portrait', 'facebook', 'x', 'linkedin', 'pinterest']).optional(),
|
|
108
|
+
languages: z.array(z.enum(['tr', 'en'])).min(1).max(2).optional(), seconds: z.number().int().min(15).max(300).optional(),
|
|
109
|
+
drawing: z.enum(['normal', 'anime', 'chinese', 'japanese', '3d', 'ink', 'watercolor', 'pastel', 'bw-ink', 'pencil', 'clean-bw']).optional(), tone: z.string().max(80).optional(),
|
|
110
|
+
music: z.string().max(80).optional().describe('auto or a music description'), voice_tr: z.string().optional(), voice_en: z.string().optional(), negative_prompt: NEG },
|
|
111
|
+
prepare: (c, a) => ({ options: clean({ languages: a.languages, platform: a.platform, maxSeconds: a.seconds, style: a.drawing, tone: a.tone, musicStyle: a.music, voices: clean({ tr: a.voice_tr, en: a.voice_en }), autoAudio: true, negative_prompt: a.negative_prompt }) }),
|
|
112
|
+
quote: async (c, a, p) => { const r = await c.call('aistudio-inkmation-quote', { story: a.story, options: p.options, cast: [] }); const t = r.tiers?.[a.tier || 'professional'] || {}
|
|
113
|
+
return quote(c, [{ item: 'script', tokens: t.script_tokens }, { item: 'voice and music', tokens: t.audio_tokens }], { balance: n(r.balance) }) },
|
|
114
|
+
run: (c, a, ctx, p) => c.call('aistudio-inkmation-create', clean({ title: a.title, story: a.story, tier: a.tier || 'professional', options: p.options, cast: [] })),
|
|
115
|
+
wait: (r) => ({ kind: 'inkmation', id: r.project_id }) }),
|
|
116
|
+
op({ name: 'inkmation_list', group: 'inkmation', action: 'list', kind: 'read', summary: 'Your Inkmation projects', description: 'Inkmation projects.', input: {},
|
|
117
|
+
run: (c) => c.call('aistudio-inkmation-list') }),
|
|
118
|
+
op({ name: 'inkmation_get', group: 'inkmation', action: 'get', kind: 'read', summary: 'An Inkmation project', description: 'Status, script, audio state and renders of an Inkmation project.',
|
|
119
|
+
input: { project_id: ID('Project id') }, positional: 'project_id', run: (c, a) => c.call('aistudio-inkmation-get', { project_id: Number(a.project_id) }) }),
|
|
120
|
+
op({ name: 'inkmation_render', group: 'inkmation', action: 'render', kind: 'spend', summary: 'Render an Inkmation film',
|
|
121
|
+
description: 'Renders a ready Inkmation project (MP4 HD/4K, WebP or GIF; optional language and drawing style switch at render time).',
|
|
122
|
+
input: { ...renderInput, project_id: ID('Inkmation project id'), lang: z.enum(['tr', 'en']).optional(), drawing: z.string().optional() }, positional: 'project_id',
|
|
123
|
+
quote: async (c, a) => { const r = await c.call('aistudio-inkmation-render-quote', clean({ ...renderBody(a), lang: a.lang, style: a.drawing })); return quote(c, [{ item: `render ${r.format} ${r.duration || ''}s`, tokens: r.tokens }], { balance: n(r.balance) }) },
|
|
124
|
+
run: async (c, a) => { const r = await c.call('aistudio-inkmation-render', clean({ ...renderBody(a), lang: a.lang, style: a.drawing })); return { render_id: String(r.render_id), status: r.status, tokens_charged: n(r.tokens_charged), wait_id: `${a.project_id}:${r.render_id}` } },
|
|
125
|
+
wait: (r) => ({ kind: 'inkmation_render', id: r.wait_id }) }),
|
|
126
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Cinema Studio (video düzenleyici): proje listesi, dışa aktarma, seslendirme, konuşan sunucu, altyazı.
|
|
2
|
+
import { z, op, ID, n, clean, opCost, quote } from './_util.js'
|
|
3
|
+
import { uploadAsset } from '../files.js'
|
|
4
|
+
|
|
5
|
+
const J = (v) => { if (v && typeof v === 'object') return v; try { return JSON.parse(v || '{}') } catch { return {} } }
|
|
6
|
+
/** Zaman çizelgesinin sonu (sn, hız dahil) — sunucunun ücretlendirdiği süre. */
|
|
7
|
+
export function timelineSeconds(data) {
|
|
8
|
+
let end = 0
|
|
9
|
+
for (const t of J(data).tracks || []) for (const it of t.items || []) {
|
|
10
|
+
const start = Number(it.start || 0)
|
|
11
|
+
let len = 0
|
|
12
|
+
if (it.dur != null) len = Number(it.dur)
|
|
13
|
+
else if (it.out != null) len = (Number(it.out) - Number(it.in || 0)) / (Number(it.speed) || 1)
|
|
14
|
+
else if (it.duration != null) len = Number(it.duration) / (Number(it.speed) || 1)
|
|
15
|
+
end = Math.max(end, start + Math.max(0, len))
|
|
16
|
+
}
|
|
17
|
+
return Math.ceil(end)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default [
|
|
21
|
+
op({ name: 'cinema_projects', group: 'cinema', action: 'list', kind: 'read', summary: 'Cinema Studio projects',
|
|
22
|
+
description: 'Cinema Studio (video editor) projects with duration, aspect and last export link.', input: {},
|
|
23
|
+
run: (c) => c.call('aistudio-cinema-project-list') }),
|
|
24
|
+
op({ name: 'cinema_render', group: 'cinema', action: 'render', kind: 'spend', summary: 'Export a Cinema Studio project to a video file',
|
|
25
|
+
description: 'Exports a Cinema Studio timeline to MP4/MOV/WebM at 720p, 1080p or 4K (2160p). Priced per second of timeline at the chosen resolution.',
|
|
26
|
+
input: { project_id: ID('Cinema project id'), resolution: z.enum(['720p', '1080p', '2160p']).optional(), format: z.enum(['mp4', 'mov', 'webm']).optional(), fps: z.number().int().min(1).max(120).optional(),
|
|
27
|
+
preset: z.enum(['youtube', 'shorts', 'ig_square', 'ig_portrait', 'linkedin']).optional() },
|
|
28
|
+
positional: 'project_id',
|
|
29
|
+
prepare: async (c, a) => { const p = await c.call('aistudio-cinema-project-get', { project_id: a.project_id }); const s = timelineSeconds(p.data); if (!s) throw new Error('Empty timeline'); return { seconds: s, res: a.resolution || '1080p' } },
|
|
30
|
+
quote: async (c, a, p) => { const x = await opCost(c, `cinema_render_${p.res}`); return quote(c, [{ item: `cinema_render_${p.res} x ${p.seconds}s`, tokens: x.tokens * p.seconds }], { balance: x.balance }) },
|
|
31
|
+
run: (c, a, ctx, p) => c.call('aistudio-cinema-render', clean({ project_id: a.project_id, format: a.format || 'mp4', resolution: p.res, fps: a.fps || 30, preset: a.preset })),
|
|
32
|
+
wait: (r) => ({ kind: 'job', id: r.job_id }) }),
|
|
33
|
+
op({ name: 'cinema_voices', group: 'cinema', action: 'voices', kind: 'read', summary: 'Narration voices of Cinema Studio (house voices + your cloned voices)',
|
|
34
|
+
description: 'House voices and your own cloned voices for Cinema Studio narration and the talking presenter.', input: {},
|
|
35
|
+
run: (c) => c.call('aistudio-cinema-voices') }),
|
|
36
|
+
op({ name: 'cinema_narrate', group: 'cinema', action: 'narrate', kind: 'spend', summary: 'Text to narration audio (saved to the library)',
|
|
37
|
+
description: 'Turns text (up to 5,000 characters) into a narration audio file with word timings, in a house voice or your cloned voice. Priced per character (minimum 100 tokens).',
|
|
38
|
+
positional: 'text',
|
|
39
|
+
input: { text: z.string().min(1).max(5000), voice: z.string().describe('House voice key (warm_female, warm_male, energetic_female, energetic_male, calm_narrator, deep_trailer) or your v:<id>'), stability: z.union([z.literal(0), z.literal(0.5), z.literal(1)]).optional() },
|
|
40
|
+
quote: async (c, a) => { const r = await c.call('aistudio-cinema-ai-rates'); return quote(c, [{ item: `narration ${a.text.length} chars`, tokens: Math.max(Number(r.min_tokens || 100), Math.ceil(Number(r.per_char) * a.text.length)) }], { balance: n(r.balance) }) },
|
|
41
|
+
run: (c, a) => c.call('aistudio-cinema-narrate', clean({ text: a.text, voice: a.voice, stability: a.stability })) }),
|
|
42
|
+
op({ name: 'cinema_presenter', group: 'cinema', action: 'presenter', kind: 'spend', summary: 'Talking presenter: a photo of a person speaks your text or audio',
|
|
43
|
+
description: 'Makes a talking presenter video from a portrait image and an audio file (2-60 s) or text + voice. Tier standard or pro. Priced per second (plus narration when text is given).',
|
|
44
|
+
input: { image_asset_id: ID('Portrait image asset id').optional(), image_path: z.string().optional(), audio_asset_id: ID('Audio asset id').optional(), text: z.string().max(1000).optional(), voice: z.string().optional(), tier: z.enum(['standard', 'pro']).optional(), prompt: z.string().max(300).optional() },
|
|
45
|
+
prepare: (c, a) => { if (!a.image_asset_id && !a.image_path) throw new Error('give image_asset_id or image_path'); if (!a.audio_asset_id && !(a.text && a.voice)) throw new Error('give audio_asset_id, or text + voice'); return { tier: a.tier || 'standard' } },
|
|
46
|
+
quote: async (c, a, p) => { const r = await c.call('aistudio-cinema-ai-rates'); const sec = a.text ? Math.min(60, Math.max(2, Math.ceil(a.text.length / 14))) : 60
|
|
47
|
+
return quote(c, [{ item: `presenter ${p.tier} ~${sec}s`, tokens: Number(r.per_presenter_sec?.[p.tier] || 0) * sec }, a.text && { item: 'narration', tokens: Math.ceil(Number(r.per_char) * a.text.length) }], { balance: n(r.balance), approximate: true, notes: a.text ? 'Seconds estimated from text length.' : 'Upper bound (60 s); charged for the actual audio length.' }) },
|
|
48
|
+
run: async (c, a, ctx, p) => { const img = a.image_asset_id ? Number(a.image_asset_id) : Number((await uploadAsset(c, a.image_path, { kind: 'image' })).asset_id)
|
|
49
|
+
return c.call('aistudio-cinema-presenter', clean({ image_asset_id: img, audio_asset_id: n(a.audio_asset_id), text: a.audio_asset_id ? undefined : a.text, voice: a.audio_asset_id ? undefined : a.voice, tier: p.tier, prompt: a.prompt })) },
|
|
50
|
+
wait: (r) => ({ kind: 'cinema_ai', id: r.job_id }) }),
|
|
51
|
+
op({ name: 'cinema_subtitles', group: 'cinema', action: 'subtitles', kind: 'spend', summary: 'Automatic subtitles for a Cinema Studio timeline',
|
|
52
|
+
description: 'Transcribes the speech on a Cinema Studio timeline and adds subtitles. Priced per second of speech.',
|
|
53
|
+
input: { project_id: ID('Cinema project id'), language: z.string().max(3).optional().describe('2-3 letter code or auto') }, positional: 'project_id',
|
|
54
|
+
prepare: async (c, a) => { const p = await c.call('aistudio-cinema-project-get', { project_id: a.project_id }); return { seconds: timelineSeconds(p.data) } },
|
|
55
|
+
quote: async (c, a, p) => { const r = await c.call('aistudio-cinema-ai-rates'); return quote(c, [{ item: `subtitles ~${p.seconds}s`, tokens: Number(r.stt_base || 0) + Math.ceil(Number(r.per_stt_sec || 0) * p.seconds) }], { balance: n(r.balance), approximate: true }) },
|
|
56
|
+
run: (c, a) => c.call('aistudio-cinema-subtitles', { project_id: a.project_id, scope: 'timeline', language: a.language || 'auto' }),
|
|
57
|
+
wait: (r) => ({ kind: 'cinema_ai', id: r.job_id }) }),
|
|
58
|
+
]
|