@nodaro/shared 3.7.0 → 3.9.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/dist/index.cjs +324 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -20
- package/dist/index.d.ts +227 -20
- package/dist/index.js +307 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/model-tree.test.ts +3 -0
- package/src/__tests__/normalize-model-input.test.ts +32 -0
- package/src/__tests__/scene3d-render-pricing.test.ts +157 -0
- package/src/__tests__/scene3d-v2.test.ts +4 -1
- package/src/__tests__/suno-credit-type.test.ts +46 -0
- package/src/credit-identifiers.ts +20 -5
- package/src/i18n/camera-motions.ar.ts +5 -5
- package/src/i18n/camera-motions.de.ts +5 -5
- package/src/i18n/camera-motions.es.ts +5 -5
- package/src/i18n/camera-motions.fr.ts +5 -5
- package/src/i18n/camera-motions.he.ts +5 -5
- package/src/i18n/camera-motions.hi.ts +5 -5
- package/src/i18n/camera-motions.ja.ts +5 -5
- package/src/i18n/camera-motions.ko.ts +5 -5
- package/src/i18n/camera-motions.pt-BR.ts +5 -5
- package/src/i18n/camera-motions.ru.ts +5 -5
- package/src/i18n/camera-motions.zh-CN.ts +5 -5
- package/src/index.ts +10 -0
- package/src/model-catalog.ts +145 -3
- package/src/model-constants.ts +98 -13
- package/src/node-default-mappings.ts +4 -0
- package/src/pipeline-types.ts +2 -0
- package/src/pro-3d-render.ts +55 -0
- package/src/reference-board-templates.ts +6 -1
- package/src/scene3d-render-pricing.ts +145 -0
- package/src/scene3d-v2.ts +3 -1
- package/src/scene3d.ts +19 -1
package/package.json
CHANGED
|
@@ -10,6 +10,9 @@ describe("modelToNodeTarget", () => {
|
|
|
10
10
|
expect(modelToNodeTarget("suno")).toEqual({ nodeType: "suno-generate", field: "model", value: "V4" })
|
|
11
11
|
expect(modelToNodeTarget("suno-v5")).toEqual({ nodeType: "suno-generate", field: "model", value: "V5" })
|
|
12
12
|
expect(modelToNodeTarget("suno-v5_5")).toEqual({ nodeType: "suno-generate", field: "model", value: "V5_5" })
|
|
13
|
+
expect(modelToNodeTarget("suno-v6")).toEqual({ nodeType: "suno-generate", field: "model", value: "V6" })
|
|
14
|
+
expect(modelToNodeTarget("suno-v6_wild")).toEqual({ nodeType: "suno-generate", field: "model", value: "V6_WILD" })
|
|
15
|
+
expect(modelToNodeTarget("suno-v6_mini")).toEqual({ nodeType: "suno-generate", field: "model", value: "V6_MINI" })
|
|
13
16
|
})
|
|
14
17
|
// `elevenlabs-dubbing` is a single-provider utility whose id is NOT in any
|
|
15
18
|
// provider-enum array, so it exercises the modes fallback → bare node, no preset.
|
|
@@ -76,6 +76,38 @@ describe("normalizeModelInput", () => {
|
|
|
76
76
|
expect(normalizeModelInput("gpt-image-2", { aspectRatio: "1:1", resolution: "4K" }).resolution).toBe("2K")
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
+
// GPT Image 2.5 is NOT GPT Image 2. Its KIE docs
|
|
80
|
+
// (docs.kie.ai/market/gpt/gpt-image-2-5-*) state no aspect_ratio x resolution
|
|
81
|
+
// restriction, so the cross-field block above must NOT be widened to these
|
|
82
|
+
// ids "for consistency" — doing so would silently downgrade a paid 4K render
|
|
83
|
+
// to 1K. If GPT Image 2.5 ever documents such a limit, add it deliberately
|
|
84
|
+
// and change this test in the same commit.
|
|
85
|
+
it.each([
|
|
86
|
+
"gpt-image-2-5-flare",
|
|
87
|
+
"gpt-image-2-5-flare-i2i",
|
|
88
|
+
"gpt-image-2-5-sunburst",
|
|
89
|
+
"gpt-image-2-5-sunburst-i2i",
|
|
90
|
+
])("%s has NO cross-field aspect-ratio x resolution rule", (modelId) => {
|
|
91
|
+
const auto4k = normalizeModelInput(modelId, { aspectRatio: "auto", resolution: "4K" })
|
|
92
|
+
expect(auto4k.resolution).toBe("4K")
|
|
93
|
+
expect(auto4k.adjustments).toEqual([])
|
|
94
|
+
|
|
95
|
+
const square4k = normalizeModelInput(modelId, { aspectRatio: "1:1", resolution: "4K" })
|
|
96
|
+
expect(square4k.resolution).toBe("4K")
|
|
97
|
+
expect(square4k.adjustments).toEqual([])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// The four ratios 2.5 introduced are new to the platform vocabulary; the snap
|
|
101
|
+
// must accept them rather than rewrite them to a neighbour.
|
|
102
|
+
it.each(["27:16", "16:27", "9:8", "8:9", "21:9", "3:2"])(
|
|
103
|
+
"gpt-image-2.5 keeps the newly-added ratio %s",
|
|
104
|
+
(ratio) => {
|
|
105
|
+
const out = normalizeModelInput("gpt-image-2-5-flare", { aspectRatio: ratio })
|
|
106
|
+
expect(out.aspectRatio).toBe(ratio)
|
|
107
|
+
expect(out.adjustments).toEqual([])
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
79
111
|
it("passes unknown model ids through untouched (the Zod enum owns those)", () => {
|
|
80
112
|
const out = normalizeModelInput("totally-fake-model", { aspectRatio: "21:9" })
|
|
81
113
|
expect(out.aspectRatio).toBe("21:9")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
PRO3D_RENDER_QUALITY_PROFILES,
|
|
4
|
+
RENDER_VIDEO_CREDIT_ID,
|
|
5
|
+
SCENE3D_LIMITS,
|
|
6
|
+
SCENE3D_RENDER_BASE_MAX_PX,
|
|
7
|
+
SCENE3D_RENDER_TIERS,
|
|
8
|
+
SCENE3D_RENDER_TIER_MULTIPLIERS,
|
|
9
|
+
SCENE3D_RENDER_XLARGE_MIN_AREA_PX,
|
|
10
|
+
pro3DRenderFrameUnit,
|
|
11
|
+
renderVideoCreditId,
|
|
12
|
+
scene3DRenderTier,
|
|
13
|
+
scene3DRenderTierCredits,
|
|
14
|
+
} from "../index.js"
|
|
15
|
+
|
|
16
|
+
/** The three reference frames the tiers are anchored to (PR #1328). */
|
|
17
|
+
const REFERENCE_FRAMES = {
|
|
18
|
+
base: { width: 1920, height: 1080 },
|
|
19
|
+
large: { width: 2560, height: 1440 },
|
|
20
|
+
xlarge: { width: 2560, height: 2560 },
|
|
21
|
+
} as const
|
|
22
|
+
|
|
23
|
+
describe("scene3DRenderTier", () => {
|
|
24
|
+
it("places each reference frame in its own tier", () => {
|
|
25
|
+
for (const [tier, frame] of Object.entries(REFERENCE_FRAMES)) {
|
|
26
|
+
expect(scene3DRenderTier(frame.width, frame.height)).toBe(tier)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("holds every pre-cap frame at the base price, square ones included", () => {
|
|
31
|
+
// The promise of the 1920 gate: raising maxDimensionPx never reprices a
|
|
32
|
+
// frame that was already renderable. 1920x1920 has the SAME pixel count as
|
|
33
|
+
// 2560x1440 and is still base — area only decides ABOVE the gate.
|
|
34
|
+
expect(scene3DRenderTier(1920, 1080)).toBe("base")
|
|
35
|
+
expect(scene3DRenderTier(1080, 1920)).toBe("base")
|
|
36
|
+
expect(scene3DRenderTier(1920, 1920)).toBe("base")
|
|
37
|
+
expect(scene3DRenderTier(1680, 720)).toBe("base")
|
|
38
|
+
expect(SCENE3D_RENDER_BASE_MAX_PX).toBe(1920)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("tiers every aspect the 2560 cap makes reachable", () => {
|
|
42
|
+
expect(scene3DRenderTier(2560, 1440)).toBe("large") // 16:9
|
|
43
|
+
expect(scene3DRenderTier(1440, 2560)).toBe("large") // 9:16
|
|
44
|
+
expect(scene3DRenderTier(2560, 1097)).toBe("large") // 21:9
|
|
45
|
+
expect(scene3DRenderTier(2048, 2560)).toBe("xlarge") // 4:5
|
|
46
|
+
expect(scene3DRenderTier(2560, 2560)).toBe("xlarge") // 1:1
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("splits large from xlarge at the midpoint of the two upper reference frames", () => {
|
|
50
|
+
const midpoint = (REFERENCE_FRAMES.large.width * REFERENCE_FRAMES.large.height + REFERENCE_FRAMES.xlarge.width * REFERENCE_FRAMES.xlarge.height) / 2
|
|
51
|
+
expect(SCENE3D_RENDER_XLARGE_MIN_AREA_PX).toBe(midpoint)
|
|
52
|
+
// One pixel either side of the boundary, at a width past the gate.
|
|
53
|
+
expect(scene3DRenderTier(2560, 2000)).toBe("large") // 5,120,000 exactly
|
|
54
|
+
expect(scene3DRenderTier(2560, 2001)).toBe("xlarge") // 5,122,560
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("is base for anything that is not a pair of positive finite numbers", () => {
|
|
58
|
+
expect(scene3DRenderTier(undefined, undefined)).toBe("base")
|
|
59
|
+
expect(scene3DRenderTier("2560", "2560")).toBe("base")
|
|
60
|
+
expect(scene3DRenderTier(Number.NaN, 2560)).toBe("base")
|
|
61
|
+
expect(scene3DRenderTier(Infinity, 2560)).toBe("base")
|
|
62
|
+
expect(scene3DRenderTier(0, 2560)).toBe("base")
|
|
63
|
+
expect(scene3DRenderTier(-2560, -2560)).toBe("base")
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("covers the whole admissible range with a declared tier", () => {
|
|
67
|
+
const max = SCENE3D_LIMITS.maxDimensionPx
|
|
68
|
+
for (const w of [SCENE3D_LIMITS.minDimensionPx, 640, 1920, 1921, 2048, max]) {
|
|
69
|
+
for (const h of [SCENE3D_LIMITS.minDimensionPx, 640, 1920, 1921, 2048, max]) {
|
|
70
|
+
expect(SCENE3D_RENDER_TIERS).toContain(scene3DRenderTier(w, h))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("scene3DRenderTierCredits", () => {
|
|
77
|
+
it("is the declared ratio, rounded up", () => {
|
|
78
|
+
// The worked examples the public docs print, from the built-in base of 50.
|
|
79
|
+
expect(scene3DRenderTierCredits(50, "base")).toBe(50)
|
|
80
|
+
expect(scene3DRenderTierCredits(50, "large")).toBe(75)
|
|
81
|
+
expect(scene3DRenderTierCredits(50, "xlarge")).toBe(125)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("never rounds a tier down below the base render it multiplies", () => {
|
|
85
|
+
for (const base of [1, 3, 7, 15, 33, 50, 137]) {
|
|
86
|
+
for (const tier of SCENE3D_RENDER_TIERS) {
|
|
87
|
+
expect(scene3DRenderTierCredits(base, tier)).toBeGreaterThanOrEqual(base)
|
|
88
|
+
expect(scene3DRenderTierCredits(base, tier)).toBe(
|
|
89
|
+
Math.ceil(base * SCENE3D_RENDER_TIER_MULTIPLIERS[tier]),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("keeps the ladder monotonic", () => {
|
|
96
|
+
expect(SCENE3D_RENDER_TIER_MULTIPLIERS.base).toBe(1)
|
|
97
|
+
expect(SCENE3D_RENDER_TIER_MULTIPLIERS.large).toBeGreaterThan(SCENE3D_RENDER_TIER_MULTIPLIERS.base)
|
|
98
|
+
expect(SCENE3D_RENDER_TIER_MULTIPLIERS.xlarge).toBeGreaterThan(SCENE3D_RENDER_TIER_MULTIPLIERS.large)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe("renderVideoCreditId", () => {
|
|
103
|
+
it("keeps the bare identifier for a base-sized 3D scene plan", () => {
|
|
104
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: { width: 1920, height: 1080 } })).toBe(
|
|
105
|
+
RENDER_VIDEO_CREDIT_ID,
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("names the tier for a plan past the 1920 gate", () => {
|
|
110
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: { width: 2560, height: 1440 } })).toBe(
|
|
111
|
+
"render-video:3d-large",
|
|
112
|
+
)
|
|
113
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: { width: 2560, height: 2560 } })).toBe(
|
|
114
|
+
"render-video:3d-xlarge",
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("leaves every non-3D render on the flat identifier", () => {
|
|
119
|
+
// scene-graph admits frames up to 3840 today at the flat price; tiering it
|
|
120
|
+
// would raise the price of work people already run.
|
|
121
|
+
expect(renderVideoCreditId({ planType: "scene-graph", plan: { width: 3840, height: 2160 } })).toBe(
|
|
122
|
+
RENDER_VIDEO_CREDIT_ID,
|
|
123
|
+
)
|
|
124
|
+
expect(renderVideoCreditId({ planType: "lottie-graphic", plan: { width: 2560, height: 2560 } })).toBe(
|
|
125
|
+
RENDER_VIDEO_CREDIT_ID,
|
|
126
|
+
)
|
|
127
|
+
expect(renderVideoCreditId({ template: "slideshow", aspectRatio: "16:9" })).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("answers the flat identifier for anything unparseable", () => {
|
|
131
|
+
expect(renderVideoCreditId(undefined)).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
132
|
+
expect(renderVideoCreditId(null)).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
133
|
+
expect(renderVideoCreditId("3d-scene")).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
134
|
+
expect(renderVideoCreditId({ planType: "3d-scene" })).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
135
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: "{}" })).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
136
|
+
expect(renderVideoCreditId({ planType: "3d-scene", plan: {} })).toBe(RENDER_VIDEO_CREDIT_ID)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe("pro3DRenderFrameUnit", () => {
|
|
141
|
+
it("keeps the configured spelling for base-sized frames", () => {
|
|
142
|
+
for (const quality of PRO3D_RENDER_QUALITY_PROFILES) {
|
|
143
|
+
expect(pro3DRenderFrameUnit(quality)).toBe(`pro-3d-render:render-frame:${quality}`)
|
|
144
|
+
expect(pro3DRenderFrameUnit(quality, "base")).toBe(`pro-3d-render:render-frame:${quality}`)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("suffixes the tier for a frame past the gate", () => {
|
|
149
|
+
expect(pro3DRenderFrameUnit("standard", "large")).toBe("pro-3d-render:render-frame:standard:large")
|
|
150
|
+
expect(pro3DRenderFrameUnit("standard", "xlarge")).toBe("pro-3d-render:render-frame:standard:xlarge")
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("produces one distinct unit per tier", () => {
|
|
154
|
+
const units = new Set(SCENE3D_RENDER_TIERS.map((tier) => pro3DRenderFrameUnit("standard", tier)))
|
|
155
|
+
expect(units.size).toBe(SCENE3D_RENDER_TIERS.length)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -108,7 +108,10 @@ describe("scene3d v2 — v1 is untouched", () => {
|
|
|
108
108
|
expect(SCENE3D_LIMITS.maxObjects).toBe(100)
|
|
109
109
|
expect(SCENE3D_LIMITS.maxKeyframes).toBe(240)
|
|
110
110
|
expect(SCENE3D_LIMITS.maxHierarchyDepth).toBe(8)
|
|
111
|
-
|
|
111
|
+
// Widened from 1920 deliberately (measured, both versions together) —
|
|
112
|
+
// every other v1 bound above is still frozen.
|
|
113
|
+
expect(SCENE3D_LIMITS.maxDimensionPx).toBe(2560)
|
|
114
|
+
expect(SCENE3D_V2_LIMITS.maxDimensionPx).toBe(SCENE3D_LIMITS.maxDimensionPx)
|
|
112
115
|
expect(SCENE3D_LIMITS.maxReferences).toBe(8)
|
|
113
116
|
})
|
|
114
117
|
|
|
@@ -1,18 +1,64 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
2
|
import {
|
|
3
3
|
sunoCreditType,
|
|
4
|
+
SUNO_VERSION_CREDIT_KEYS,
|
|
4
5
|
SUNO_VERSION_PRICED_OPERATIONS,
|
|
5
6
|
SUNO_SELECT_OPERATIONS,
|
|
6
7
|
SUNO_MODELS,
|
|
8
|
+
SUNO_ACTIVE_MODELS,
|
|
9
|
+
SUNO_LEGACY_MODELS,
|
|
10
|
+
SUNO_ADD_TRACK_MODELS,
|
|
11
|
+
DEFAULT_SUNO_MODEL,
|
|
12
|
+
sunoModelHonoursDuration,
|
|
13
|
+
MODEL_CATALOG,
|
|
7
14
|
} from "../index.js"
|
|
8
15
|
|
|
9
16
|
describe("sunoCreditType — the ONE implementation of the Suno route pricing contract", () => {
|
|
10
17
|
it("prices generate/cover/extend by model version", () => {
|
|
18
|
+
expect(sunoCreditType("V6", "suno-generate")).toBe("suno-v6")
|
|
19
|
+
expect(sunoCreditType("V6_WILD", "suno-cover")).toBe("suno-v6_wild")
|
|
20
|
+
expect(sunoCreditType("V6_MINI", "suno-extend")).toBe("suno-v6_mini")
|
|
11
21
|
expect(sunoCreditType("V5_5", "suno-generate")).toBe("suno-v5_5")
|
|
12
22
|
expect(sunoCreditType("V5", "suno-cover")).toBe("suno-v5")
|
|
13
23
|
expect(sunoCreditType("V5_5", "suno-extend")).toBe("suno-v5_5")
|
|
14
24
|
})
|
|
15
25
|
|
|
26
|
+
// A new active version with no key would silently bill at the operation
|
|
27
|
+
// key — and be invisible in /admin/models. Every active version must own a
|
|
28
|
+
// key, and that key must be the pricing identifier of the catalog entry
|
|
29
|
+
// whose dataValue is that version (the Models tab + /v1/models contract).
|
|
30
|
+
it("gives every ACTIVE version its own credit key, matching its catalog entry", () => {
|
|
31
|
+
const sunoEntries = Object.values(MODEL_CATALOG).filter((m) => m.family === "Suno")
|
|
32
|
+
for (const model of SUNO_ACTIVE_MODELS) {
|
|
33
|
+
const key = SUNO_VERSION_CREDIT_KEYS[model]
|
|
34
|
+
expect(key, `${model} has no SUNO_VERSION_CREDIT_KEYS row`).toBeTruthy()
|
|
35
|
+
for (const op of SUNO_VERSION_PRICED_OPERATIONS) {
|
|
36
|
+
expect(sunoCreditType(model, op)).toBe(key)
|
|
37
|
+
}
|
|
38
|
+
const entry = sunoEntries.find((m) => m.dataValue === model)
|
|
39
|
+
expect(entry, `${model} has no MODEL_CATALOG entry with dataValue`).toBeTruthy()
|
|
40
|
+
expect(entry!.pricing.map((p) => p.identifier)).toContain(key)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("keeps the model lists active-first with the default at the head", () => {
|
|
45
|
+
expect([...SUNO_ACTIVE_MODELS]).toEqual(["V6", "V6_WILD", "V6_MINI"])
|
|
46
|
+
expect(SUNO_MODELS[0]).toBe(DEFAULT_SUNO_MODEL)
|
|
47
|
+
expect([...SUNO_MODELS]).toEqual([...SUNO_ACTIVE_MODELS, ...SUNO_LEGACY_MODELS])
|
|
48
|
+
// The two halves never overlap.
|
|
49
|
+
for (const legacy of SUNO_LEGACY_MODELS) {
|
|
50
|
+
expect(SUNO_ACTIVE_MODELS as readonly string[]).not.toContain(legacy)
|
|
51
|
+
}
|
|
52
|
+
for (const m of SUNO_ADD_TRACK_MODELS) expect(SUNO_MODELS as readonly string[]).toContain(m)
|
|
53
|
+
for (const m of SUNO_ACTIVE_MODELS) expect(SUNO_ADD_TRACK_MODELS as readonly string[]).toContain(m)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("honours duration on the V6 family only (V5_5 was struck from the docs sentence)", () => {
|
|
57
|
+
for (const m of SUNO_ACTIVE_MODELS) expect(sunoModelHonoursDuration(m)).toBe(true)
|
|
58
|
+
for (const m of SUNO_LEGACY_MODELS) expect(sunoModelHonoursDuration(m)).toBe(false)
|
|
59
|
+
expect(sunoModelHonoursDuration(undefined)).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
16
62
|
it("falls back to the operation key for versions with no dedicated price", () => {
|
|
17
63
|
expect(sunoCreditType("V4", "suno-generate")).toBe("suno-generate")
|
|
18
64
|
expect(sunoCreditType("V4_5ALL", "suno-cover")).toBe("suno-cover")
|
|
@@ -598,14 +598,29 @@ export const SUNO_SELECT_OPERATIONS = [
|
|
|
598
598
|
* node badges. It is a billing key — NEVER a KIE provider id — which is what
|
|
599
599
|
* satisfies the B3 egress invariant.
|
|
600
600
|
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
601
|
+
* The version → key map is {@link SUNO_VERSION_CREDIT_KEYS}; a version with no
|
|
602
|
+
* entry (V4 / V4_5 / V4_5ALL / V4_5PLUS) falls back to the operation key, and a
|
|
603
|
+
* non-version-priced operation is returned unchanged.
|
|
603
604
|
*/
|
|
604
605
|
export function sunoCreditType(model: string | undefined, operation: string): string {
|
|
605
606
|
if (!(SUNO_VERSION_PRICED_OPERATIONS as readonly string[]).includes(operation)) {
|
|
606
607
|
return operation
|
|
607
608
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
609
|
+
return (model && SUNO_VERSION_CREDIT_KEYS[model]) || operation
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Suno version → Nodaro credit key for the version-priced operations. One row
|
|
614
|
+
* per version that has its own `model_pricing` row / `STATIC_CREDIT_COSTS`
|
|
615
|
+
* entry / `MODEL_CATALOG` pricing row, so an admin can reprice one version
|
|
616
|
+
* without touching code. Every ACTIVE version MUST have a row here — guarded
|
|
617
|
+
* by `__tests__/suno-credit-type.test.ts` (a new active version with no key
|
|
618
|
+
* would silently bill at the operation key).
|
|
619
|
+
*/
|
|
620
|
+
export const SUNO_VERSION_CREDIT_KEYS: Readonly<Record<string, string>> = {
|
|
621
|
+
V6: "suno-v6",
|
|
622
|
+
V6_WILD: "suno-v6_wild",
|
|
623
|
+
V6_MINI: "suno-v6_mini",
|
|
624
|
+
V5_5: "suno-v5_5",
|
|
625
|
+
V5: "suno-v5",
|
|
611
626
|
}
|
|
@@ -22,8 +22,8 @@ const map: LocaleCatalogMap = {
|
|
|
22
22
|
"dolly-in": { label: "Dolly In", description: "دفع الكاميرا نحو الموضوع (تباين منظور)" },
|
|
23
23
|
"dolly-out": { label: "Dolly Out", description: "سحب الكاميرا للخلف (تباين منظور)" },
|
|
24
24
|
"dolly-zoom": { label: "Dolly Zoom", description: "تأثير Vertigo: Dolly يعاكس Zoom" },
|
|
25
|
-
"push-in": { label: "Push In", description: "دفع
|
|
26
|
-
"pull-out": { label: "Pull Out", description: "سحب
|
|
25
|
+
"push-in": { label: "Push In", description: "دفع سريع قوي نحو الموضوع" },
|
|
26
|
+
"pull-out": { label: "Pull Out", description: "سحب سريع قوي عن الموضوع" },
|
|
27
27
|
"breathing": { label: "كاميرا تتنفس", description: "تذبذب مستمر خفيف من الدفع والسحب، إحساس عضوي بالكاميرا المحمولة باليد" },
|
|
28
28
|
"push-pull": { label: "Push-Pull / تأرجح", description: "تتحرك الكاميرا نحو الموضوع ثم تعود للخلف، اقتراب وانسحاب متأرجح" },
|
|
29
29
|
"creep-in": { label: "Creep-In", description: "Push-in بطيء بشكل غير محسوس يبني الرعب أو التوتر" },
|
|
@@ -39,8 +39,8 @@ const map: LocaleCatalogMap = {
|
|
|
39
39
|
"roll-right": { label: "Roll لليمين", description: "تدوير الكاميرا مع عقارب الساعة" },
|
|
40
40
|
"dutch-angle": { label: "Dutch Angle", description: "إطار ثابت مائل للتوتر" },
|
|
41
41
|
|
|
42
|
-
"orbit-left": { label: "دوران Orbit يسار", description: "
|
|
43
|
-
"orbit-right": { label: "دوران Orbit يمين", description: "
|
|
42
|
+
"orbit-left": { label: "دوران Orbit يسار", description: "دوران جزئي واسع حول الموضوع لليسار" },
|
|
43
|
+
"orbit-right": { label: "دوران Orbit يمين", description: "دوران جزئي واسع حول الموضوع لليمين" },
|
|
44
44
|
"spin-360": { label: "دوران كامل 360°", description: "تدور الكاميرا 360 درجة كاملة حول محورها" },
|
|
45
45
|
"orbit-360": { label: "Orbit كامل 360°", description: "تتحرك الكاميرا في قوس كامل 360 درجة حول الموضوع" },
|
|
46
46
|
"arc-left": { label: "قوس Arc يسار", description: "قوس جزئي حول الموضوع لليسار" },
|
|
@@ -75,7 +75,7 @@ const map: LocaleCatalogMap = {
|
|
|
75
75
|
"handheld-vlog": { label: "Handheld Vlog", description: "محمول باليد بأسلوب Vlog عفوي" },
|
|
76
76
|
"pov-walk": { label: "POV مشي", description: "مشي بمنظور الشخص الأول" },
|
|
77
77
|
"velocity-edit": { label: "Velocity Edit", description: "إيقاع Speed-Ramp بأسلوب TikTok" },
|
|
78
|
-
"match-cut-zoom": { label: "Match Cut Zoom", description: "تقريب
|
|
78
|
+
"match-cut-zoom": { label: "Match Cut Zoom", description: "تقريب سريع مع قطع حاد إلى شكل مطابق" },
|
|
79
79
|
"screen-tap": { label: "Screen Tap", description: "انتقال نقر إصبع على الشاشة" },
|
|
80
80
|
"phone-flip": { label: "قلب الهاتف", description: "قلب الكاميرا الأمامية/الخلفية" },
|
|
81
81
|
// Location-studio extension (PR #2505 follow-up)
|
|
@@ -18,8 +18,8 @@ const map: LocaleCatalogMap = {
|
|
|
18
18
|
"dolly-in": { label: "Dolly hinein", description: "Kamera zum Motiv schieben (Parallaxe)" },
|
|
19
19
|
"dolly-out": { label: "Dolly heraus", description: "Kamera wegziehen (Parallaxe)" },
|
|
20
20
|
"dolly-zoom": { description: "Vertigo-Effekt: Dolly entgegen Zoom" },
|
|
21
|
-
"push-in": { label: "Push-In", description: "
|
|
22
|
-
"pull-out": { label: "Pull-Out", description: "
|
|
21
|
+
"push-in": { label: "Push-In", description: "Schnelles kraftvolles Schieben zum Motiv" },
|
|
22
|
+
"pull-out": { label: "Pull-Out", description: "Schnelles kraftvolles Zurückziehen vom Motiv" },
|
|
23
23
|
"breathing": { label: "Atmende Kamera", description: "Subtile, kontinuierliche Push-In/Pull-Out-Oszillation, organisches Handkamera-Gefühl" },
|
|
24
24
|
"push-pull": { label: "Push-Pull / Schwingen", description: "Kamera bewegt sich zum Motiv und wieder zurück, schwingende Annäherung und Rückzug" },
|
|
25
25
|
"creep-in": { label: "Creep-In", description: "Unmerklich langsames Heranschieben, baut Bedrohung oder Spannung auf" },
|
|
@@ -31,8 +31,8 @@ const map: LocaleCatalogMap = {
|
|
|
31
31
|
"roll-left": { label: "Rollen nach links", description: "Kamera gegen den Uhrzeigersinn drehen" },
|
|
32
32
|
"roll-right": { label: "Rollen nach rechts", description: "Kamera im Uhrzeigersinn drehen" },
|
|
33
33
|
"dutch-angle": { description: "Statischer geneigter Bildausschnitt für Spannung" },
|
|
34
|
-
"orbit-left": { label: "Orbit nach links", description: "
|
|
35
|
-
"orbit-right": { label: "Orbit nach rechts", description: "
|
|
34
|
+
"orbit-left": { label: "Orbit nach links", description: "Großer Teil-Orbit um das Motiv nach links" },
|
|
35
|
+
"orbit-right": { label: "Orbit nach rechts", description: "Großer Teil-Orbit um das Motiv nach rechts" },
|
|
36
36
|
"spin-360": { label: "Volle 360°-Drehung", description: "Kamera dreht sich vollständig um 360 Grad um die eigene Achse" },
|
|
37
37
|
"orbit-360": { label: "Voller 360°-Orbit", description: "Kamera beschreibt einen vollständigen 360-Grad-Bogen um das Motiv" },
|
|
38
38
|
"arc-left": { label: "Bogen nach links", description: "Teilbogen um das Motiv nach links" },
|
|
@@ -63,7 +63,7 @@ const map: LocaleCatalogMap = {
|
|
|
63
63
|
"handheld-vlog": { label: "Handgehaltenes Vlog", description: "Lässige vlog-artige Handkamera" },
|
|
64
64
|
"pov-walk": { label: "POV-Gehen", description: "First-Person-Walking-POV" },
|
|
65
65
|
"velocity-edit": { description: "TikTok-Speed-Ramp-Tempo" },
|
|
66
|
-
"match-cut-zoom": { description: "
|
|
66
|
+
"match-cut-zoom": { description: "Schneller Zoom mit hartem Schnitt auf eine passende Form" },
|
|
67
67
|
"screen-tap": { label: "Screen-Tap", description: "On-Screen-Fingertipp-Übergang" },
|
|
68
68
|
"phone-flip": { label: "Phone-Flip", description: "Front-/Rückkamera-Wechsel" },
|
|
69
69
|
// Location-studio extension (PR #2505 follow-up)
|
|
@@ -22,8 +22,8 @@ const map: LocaleCatalogMap = {
|
|
|
22
22
|
"dolly-in": { label: "Dolly In", description: "Empujar la cámara hacia el sujeto (paralaje)" },
|
|
23
23
|
"dolly-out": { label: "Dolly Out", description: "Alejar la cámara (paralaje)" },
|
|
24
24
|
"dolly-zoom": { label: "Dolly Zoom", description: "Efecto vértigo: dolly opuesto al zoom" },
|
|
25
|
-
"push-in": { label: "Empuje", description: "Empuje
|
|
26
|
-
"pull-out": { label: "Retroceso", description: "Retroceso
|
|
25
|
+
"push-in": { label: "Empuje", description: "Empuje rápido y enérgico hacia el sujeto" },
|
|
26
|
+
"pull-out": { label: "Retroceso", description: "Retroceso rápido y enérgico desde el sujeto" },
|
|
27
27
|
"breathing": { label: "Cámara Respirante", description: "Oscilación sutil y continua de empuje y retroceso, sensación orgánica de cámara en mano" },
|
|
28
28
|
"push-pull": { label: "Push-Pull / Vaivén", description: "La cámara se mueve hacia el sujeto y luego se aleja, una aproximación y retirada oscilante" },
|
|
29
29
|
"creep-in": { label: "Acercamiento Imperceptible", description: "Empuje extremadamente lento e imperceptible que crea pavor o tensión" },
|
|
@@ -39,8 +39,8 @@ const map: LocaleCatalogMap = {
|
|
|
39
39
|
"roll-right": { label: "Rotación a la Derecha", description: "Rotar la cámara en sentido horario" },
|
|
40
40
|
"dutch-angle": { description: "Cuadro inclinado estático para tensión" },
|
|
41
41
|
|
|
42
|
-
"orbit-left": { label: "Órbita a la Izquierda", description: "
|
|
43
|
-
"orbit-right": { label: "Órbita a la Derecha", description: "
|
|
42
|
+
"orbit-left": { label: "Órbita a la Izquierda", description: "Órbita parcial amplia alrededor del sujeto a la izquierda" },
|
|
43
|
+
"orbit-right": { label: "Órbita a la Derecha", description: "Órbita parcial amplia alrededor del sujeto a la derecha" },
|
|
44
44
|
"spin-360": { label: "Giro Completo 360°", description: "La cámara rota 360 grados completos sobre su eje" },
|
|
45
45
|
"orbit-360": { label: "Órbita Completa 360°", description: "La cámara describe un arco completo de 360 grados alrededor del sujeto" },
|
|
46
46
|
"arc-left": { label: "Arco a la Izquierda", description: "Arco parcial alrededor del sujeto a la izquierda" },
|
|
@@ -75,7 +75,7 @@ const map: LocaleCatalogMap = {
|
|
|
75
75
|
"handheld-vlog": { label: "Vlog en Mano", description: "Cámara casual estilo vlog en mano" },
|
|
76
76
|
"pov-walk": { label: "POV Caminando", description: "POV caminando en primera persona" },
|
|
77
77
|
"velocity-edit": { label: "Edición de Velocidad", description: "Ritmo con rampa de velocidad estilo TikTok" },
|
|
78
|
-
"match-cut-zoom": { label: "Zoom de Match Cut", description: "Zoom
|
|
78
|
+
"match-cut-zoom": { label: "Zoom de Match Cut", description: "Zoom rápido con corte seco a una forma coincidente" },
|
|
79
79
|
"screen-tap": { label: "Toque de Pantalla", description: "Transición con toque de dedo en pantalla" },
|
|
80
80
|
"phone-flip": { label: "Cambio de Cámara del Teléfono", description: "Cambio entre cámara frontal y trasera" },
|
|
81
81
|
// Location-studio extension (PR #2505 follow-up)
|
|
@@ -23,8 +23,8 @@ const map: LocaleCatalogMap = {
|
|
|
23
23
|
"dolly-in": { label: "Travelling avant", description: "Pousser la caméra vers le sujet (parallaxe)" },
|
|
24
24
|
"dolly-out": { label: "Travelling arrière", description: "Reculer la caméra (parallaxe)" },
|
|
25
25
|
"dolly-zoom": { description: "Effet vertigo : travelling opposé au zoom" },
|
|
26
|
-
"push-in": { label: "Push in", description: "Poussée
|
|
27
|
-
"pull-out": { label: "Pull out", description: "Recul
|
|
26
|
+
"push-in": { label: "Push in", description: "Poussée rapide et énergique vers le sujet" },
|
|
27
|
+
"pull-out": { label: "Pull out", description: "Recul rapide et énergique depuis le sujet" },
|
|
28
28
|
"breathing": { label: "Caméra respirante", description: "Oscillation continue et subtile de poussée et recul, sensation organique caméra à l'épaule" },
|
|
29
29
|
"push-pull": { label: "Push-Pull / Va-et-vient", description: "La caméra avance vers le sujet puis recule, approche-retrait oscillant" },
|
|
30
30
|
"creep-in": { label: "Approche imperceptible", description: "Poussée extrêmement lente et imperceptible qui installe la peur ou la tension" },
|
|
@@ -40,8 +40,8 @@ const map: LocaleCatalogMap = {
|
|
|
40
40
|
"roll-right": { label: "Roulis à droite", description: "Pivoter la caméra dans le sens horaire" },
|
|
41
41
|
"dutch-angle": { description: "Cadre incliné statique pour la tension" },
|
|
42
42
|
// Orbit
|
|
43
|
-
"orbit-left": { label: "Orbite à gauche", description: "
|
|
44
|
-
"orbit-right": { label: "Orbite à droite", description: "
|
|
43
|
+
"orbit-left": { label: "Orbite à gauche", description: "Grande orbite partielle autour du sujet vers la gauche" },
|
|
44
|
+
"orbit-right": { label: "Orbite à droite", description: "Grande orbite partielle autour du sujet vers la droite" },
|
|
45
45
|
"spin-360": { label: "Tour complet 360°", description: "La caméra effectue un tour complet de 360 degrés sur son axe" },
|
|
46
46
|
"orbit-360": { label: "Orbite complète 360°", description: "La caméra décrit un arc complet de 360 degrés autour du sujet" },
|
|
47
47
|
"arc-left": { label: "Arc à gauche", description: "Arc partiel autour du sujet à gauche" },
|
|
@@ -75,7 +75,7 @@ const map: LocaleCatalogMap = {
|
|
|
75
75
|
"handheld-vlog": { label: "Vlog à l'épaule", description: "Caméra à l'épaule décontractée style vlog" },
|
|
76
76
|
"pov-walk": { label: "Marche en POV", description: "POV de marche à la première personne" },
|
|
77
77
|
"velocity-edit": { description: "Cadence en speed-ramp style TikTok" },
|
|
78
|
-
"match-cut-zoom": { description: "Zoom
|
|
78
|
+
"match-cut-zoom": { description: "Zoom rapide avec coupe franche sur une forme identique" },
|
|
79
79
|
"screen-tap": { description: "Transition tap à l'écran à l'image" },
|
|
80
80
|
"phone-flip": { description: "Bascule entre caméra avant et arrière" },
|
|
81
81
|
// Location-studio extension (PR #2505 follow-up)
|
|
@@ -22,8 +22,8 @@ const map: LocaleCatalogMap = {
|
|
|
22
22
|
"dolly-in": { label: "Dolly In", description: "דחף מצלמה לעבר הסובייקט (פרלקסה)" },
|
|
23
23
|
"dolly-out": { label: "Dolly Out", description: "משוך מצלמה הרחק (פרלקסה)" },
|
|
24
24
|
"dolly-zoom": { label: "Dolly Zoom", description: "אפקט ורטיגו: dolly מנוגד ל-zoom" },
|
|
25
|
-
"push-in": { label: "Push In", description: "דחיפה
|
|
26
|
-
"pull-out": { label: "Pull Out", description: "משיכה
|
|
25
|
+
"push-in": { label: "Push In", description: "דחיפה מהירה ונמרצת לעבר הסובייקט" },
|
|
26
|
+
"pull-out": { label: "Pull Out", description: "משיכה מהירה ונמרצת מהסובייקט" },
|
|
27
27
|
"breathing": { label: "מצלמה נושמת", description: "תנודה עדינה ורציפה של push-in / pull-out, תחושת handheld אורגנית" },
|
|
28
28
|
"push-pull": { label: "Push-Pull / נדנוד", description: "המצלמה נעה לעבר הסובייקט וחזרה אחורה, גישה והתרחקות מתנדנדות" },
|
|
29
29
|
"creep-in": { label: "Creep-In", description: "Push-in איטי באופן בלתי מורגש, בונה אימה או מתח" },
|
|
@@ -39,8 +39,8 @@ const map: LocaleCatalogMap = {
|
|
|
39
39
|
"roll-right": { label: "Roll ימינה", description: "סובב מצלמה עם כיוון השעון" },
|
|
40
40
|
"dutch-angle": { label: "Dutch Angle", description: "פריים מוטה סטטי ליצירת מתח" },
|
|
41
41
|
|
|
42
|
-
"orbit-left": { label: "Orbit שמאלה", description: "
|
|
43
|
-
"orbit-right": { label: "Orbit ימינה", description: "
|
|
42
|
+
"orbit-left": { label: "Orbit שמאלה", description: "הקפה חלקית רחבה סביב הסובייקט שמאלה" },
|
|
43
|
+
"orbit-right": { label: "Orbit ימינה", description: "הקפה חלקית רחבה סביב הסובייקט ימינה" },
|
|
44
44
|
"spin-360": { label: "סיבוב מלא 360°", description: "המצלמה מסתובבת 360 מעלות מלאות סביב צירה" },
|
|
45
45
|
"orbit-360": { label: "Orbit מלא 360°", description: "המצלמה מתארת קשת מלאה של 360 מעלות סביב הסובייקט" },
|
|
46
46
|
"arc-left": { label: "Arc שמאלה", description: "קשת חלקית סביב הסובייקט שמאלה" },
|
|
@@ -75,7 +75,7 @@ const map: LocaleCatalogMap = {
|
|
|
75
75
|
"handheld-vlog": { label: "Vlog יד", description: "מצלמת יד קז'ואל בסגנון vlog" },
|
|
76
76
|
"pov-walk": { label: "POV הליכה", description: "POV הליכה בגוף ראשון" },
|
|
77
77
|
"velocity-edit": { label: "Velocity Edit", description: "קצב Speed-ramp של TikTok" },
|
|
78
|
-
"match-cut-zoom": { label: "Match Cut Zoom", description: "Zoom
|
|
78
|
+
"match-cut-zoom": { label: "Match Cut Zoom", description: "Zoom מהיר עם חיתוך חד לצורה תואמת" },
|
|
79
79
|
"screen-tap": { label: "Screen Tap", description: "מעבר עם נקישת אצבע על המסך" },
|
|
80
80
|
"phone-flip": { label: "היפוך טלפון", description: "היפוך מצלמה קדמית/אחורית" },
|
|
81
81
|
// Location-studio extension (PR #2505 follow-up)
|
|
@@ -27,8 +27,8 @@ const map: LocaleCatalogMap = {
|
|
|
27
27
|
"dolly-in": { label: "Dolly In", description: "camera को subject की ओर धकेलें (parallax)" },
|
|
28
28
|
"dolly-out": { label: "Dolly Out", description: "camera को दूर खींचें (parallax)" },
|
|
29
29
|
"dolly-zoom": { label: "Dolly Zoom", description: "Vertigo प्रभाव: dolly zoom के विपरीत" },
|
|
30
|
-
"push-in": { label: "Push In", description: "subject की ओर
|
|
31
|
-
"pull-out": { label: "Pull Out", description: "subject से
|
|
30
|
+
"push-in": { label: "Push In", description: "subject की ओर तेज़ ज़ोरदार push" },
|
|
31
|
+
"pull-out": { label: "Pull Out", description: "subject से तेज़ ज़ोरदार pull back" },
|
|
32
32
|
"breathing": { label: "Breathing Camera", description: "सूक्ष्म निरंतर push-in / pull-out दोलन, organic handheld अनुभव" },
|
|
33
33
|
"push-pull": { label: "Push-Pull / Swing", description: "Camera subject की ओर बढ़ता है फिर पीछे, झूलता हुआ approach-and-retreat" },
|
|
34
34
|
"creep-in": { label: "Creep-In", description: "अदृश्य रूप से धीमा push-in जो भय या तनाव बढ़ाता है" },
|
|
@@ -48,8 +48,8 @@ const map: LocaleCatalogMap = {
|
|
|
48
48
|
"dutch-angle": { label: "Dutch Angle", description: "तनाव के लिए स्थिर tilted frame" },
|
|
49
49
|
|
|
50
50
|
// Orbit / Arc
|
|
51
|
-
"orbit-left": { label: "Orbit बाएँ", description: "subject के चारों ओर बाएँ
|
|
52
|
-
"orbit-right": { label: "Orbit दाएँ", description: "subject के चारों ओर दाएँ
|
|
51
|
+
"orbit-left": { label: "Orbit बाएँ", description: "subject के चारों ओर बाएँ बड़ा आंशिक orbit" },
|
|
52
|
+
"orbit-right": { label: "Orbit दाएँ", description: "subject के चारों ओर दाएँ बड़ा आंशिक orbit" },
|
|
53
53
|
"spin-360": { label: "Full 360 Spin", description: "Camera अपनी धुरी पर पूरे 360 degree घूमता है" },
|
|
54
54
|
"orbit-360": { label: "Full 360 Orbit", description: "Camera subject के चारों ओर पूरे 360 degree का arc बनाता है" },
|
|
55
55
|
"arc-left": { label: "Arc बाएँ", description: "subject के चारों ओर बाएँ आंशिक arc" },
|
|
@@ -88,7 +88,7 @@ const map: LocaleCatalogMap = {
|
|
|
88
88
|
"handheld-vlog": { label: "Handheld Vlog", description: "Casual vlog-शैली का handheld" },
|
|
89
89
|
"pov-walk": { label: "POV Walk", description: "प्रथम-व्यक्ति चलते हुए POV" },
|
|
90
90
|
"velocity-edit": { label: "Velocity Edit", description: "TikTok speed-ramp pacing" },
|
|
91
|
-
"match-cut-zoom": { label: "Match Cut Zoom", description: "
|
|
91
|
+
"match-cut-zoom": { label: "Match Cut Zoom", description: "मेल खाती आकृति पर hard cut वाला तेज़ zoom" },
|
|
92
92
|
"screen-tap": { label: "Screen Tap", description: "On-screen finger-tap transition" },
|
|
93
93
|
"phone-flip": { label: "Phone Flip", description: "आगे/पीछे camera flip" },
|
|
94
94
|
// Location-studio extension (PR #2505 follow-up)
|
|
@@ -27,8 +27,8 @@ const map: LocaleCatalogMap = {
|
|
|
27
27
|
"dolly-in": { label: "ドリー・イン", description: "被写体に向かってカメラを押し込む(パララックスあり)" },
|
|
28
28
|
"dolly-out": { label: "ドリー・アウト", description: "カメラを引いていく(パララックスあり)" },
|
|
29
29
|
"dolly-zoom": { label: "ドリーズーム", description: "ヴァーティゴ効果:ドリーがズームと逆方向" },
|
|
30
|
-
"push-in": { label: "プッシュイン", description: "
|
|
31
|
-
"pull-out": { label: "プルアウト", description: "
|
|
30
|
+
"push-in": { label: "プッシュイン", description: "被写体への速く力強い押し込み" },
|
|
31
|
+
"pull-out": { label: "プルアウト", description: "被写体からの速く力強い引き戻し" },
|
|
32
32
|
"breathing": { label: "ブリージング・カメラ", description: "繊細で連続的なプッシュインとプルアウトの揺らぎ、有機的な手持ちの感触" },
|
|
33
33
|
"push-pull": { label: "プッシュプル/スウィング", description: "カメラが被写体に近づいてから離れる、振り子のような接近と後退" },
|
|
34
34
|
"creep-in": { label: "クリープイン", description: "ほとんど気づかれないほどゆっくりのプッシュイン、恐怖や緊張を高める" },
|
|
@@ -48,8 +48,8 @@ const map: LocaleCatalogMap = {
|
|
|
48
48
|
"dutch-angle": { label: "ダッチアングル", description: "緊張感を与える固定の傾いたフレーム" },
|
|
49
49
|
|
|
50
50
|
// Orbit
|
|
51
|
-
"orbit-left": { label: "オービット・レフト", description: "
|
|
52
|
-
"orbit-right": { label: "オービット・ライト", description: "
|
|
51
|
+
"orbit-left": { label: "オービット・レフト", description: "被写体の周りを左に大きく部分的に回る" },
|
|
52
|
+
"orbit-right": { label: "オービット・ライト", description: "被写体の周りを右に大きく部分的に回る" },
|
|
53
53
|
"spin-360": { label: "フル360°スピン", description: "カメラが軸上で360度フル回転する" },
|
|
54
54
|
"orbit-360": { label: "フル360°オービット", description: "カメラが被写体の周りを360度の完全な弧で回る" },
|
|
55
55
|
"arc-left": { label: "アーク・レフト", description: "被写体の周りを左に部分的に弧を描く" },
|
|
@@ -88,7 +88,7 @@ const map: LocaleCatalogMap = {
|
|
|
88
88
|
"handheld-vlog": { label: "ハンドヘルド・ヴログ", description: "カジュアルなヴログ風の手持ち" },
|
|
89
89
|
"pov-walk": { label: "POVウォーク", description: "一人称の歩行POV" },
|
|
90
90
|
"velocity-edit": { label: "ベロシティエディット", description: "TikTok風のスピードランプ・ペーシング" },
|
|
91
|
-
"match-cut-zoom": { label: "マッチカット・ズーム", description: "
|
|
91
|
+
"match-cut-zoom": { label: "マッチカット・ズーム", description: "同じ形にハードカットする速いズーム" },
|
|
92
92
|
"screen-tap": { label: "スクリーンタップ", description: "画面上の指タップによるトランジション" },
|
|
93
93
|
"phone-flip": { label: "フォンフリップ", description: "前面/背面カメラの切り替え" },
|
|
94
94
|
// Location-studio extension (PR #2505 follow-up)
|
|
@@ -27,8 +27,8 @@ const map: LocaleCatalogMap = {
|
|
|
27
27
|
"dolly-in": { label: "달리 인", description: "피사체를 향해 카메라를 밀어 넣습니다 (시차)" },
|
|
28
28
|
"dolly-out": { label: "달리 아웃", description: "카메라를 멀리 밀어냅니다 (시차)" },
|
|
29
29
|
"dolly-zoom": { label: "달리 줌", description: "버티고 효과: 달리와 줌이 반대로 동작합니다" },
|
|
30
|
-
"push-in": { label: "푸시 인", description: "피사체를 향한
|
|
31
|
-
"pull-out": { label: "풀 아웃", description: "피사체로부터
|
|
30
|
+
"push-in": { label: "푸시 인", description: "피사체를 향한 빠르고 힘찬 푸시입니다" },
|
|
31
|
+
"pull-out": { label: "풀 아웃", description: "피사체로부터 빠르고 힘찬 풀백입니다" },
|
|
32
32
|
"breathing": { label: "브리딩 카메라", description: "은은하게 지속되는 푸시 인 / 풀 아웃의 진동, 유기적인 핸드헬드 느낌입니다" },
|
|
33
33
|
"push-pull": { label: "푸시 풀 / 스윙", description: "카메라가 피사체를 향해 다가갔다가 다시 멀어지는, 흔들리는 접근과 후퇴입니다" },
|
|
34
34
|
"creep-in": { label: "크리프 인", description: "감지하기 어려울 만큼 느린 푸시 인으로 공포감이나 긴장감을 쌓아갑니다" },
|
|
@@ -48,8 +48,8 @@ const map: LocaleCatalogMap = {
|
|
|
48
48
|
"dutch-angle": { label: "더치 앵글", description: "긴장감을 주는 정적 기울어진 프레임입니다" },
|
|
49
49
|
|
|
50
50
|
// Orbit / Arc
|
|
51
|
-
"orbit-left": { label: "오비트 왼쪽", description: "피사체 주위를 왼쪽으로
|
|
52
|
-
"orbit-right": { label: "오비트 오른쪽", description: "피사체 주위를 오른쪽으로
|
|
51
|
+
"orbit-left": { label: "오비트 왼쪽", description: "피사체 주위를 왼쪽으로 크게 부분 선회합니다" },
|
|
52
|
+
"orbit-right": { label: "오비트 오른쪽", description: "피사체 주위를 오른쪽으로 크게 부분 선회합니다" },
|
|
53
53
|
"spin-360": { label: "풀 360° 스핀", description: "카메라가 자신의 축을 중심으로 360도 완전 회전합니다" },
|
|
54
54
|
"orbit-360": { label: "풀 360° 오비트", description: "카메라가 피사체 주위를 360도 완전한 호로 돕니다" },
|
|
55
55
|
"arc-left": { label: "아크 왼쪽", description: "피사체 주위를 왼쪽으로 부분적으로 도는 호입니다" },
|
|
@@ -88,7 +88,7 @@ const map: LocaleCatalogMap = {
|
|
|
88
88
|
"handheld-vlog": { label: "핸드헬드 블로그", description: "캐주얼한 블로그 스타일 핸드헬드입니다" },
|
|
89
89
|
"pov-walk": { label: "POV 워크", description: "1인칭 보행 POV입니다" },
|
|
90
90
|
"velocity-edit": { label: "벨로시티 에디트", description: "TikTok 스피드 램프 페이싱입니다" },
|
|
91
|
-
"match-cut-zoom": { label: "매치 컷 줌", description: "
|
|
91
|
+
"match-cut-zoom": { label: "매치 컷 줌", description: "일치하는 형태로 하드 컷하는 빠른 줌입니다" },
|
|
92
92
|
"screen-tap": { label: "스크린 탭", description: "화면 위 손가락 탭 트랜지션입니다" },
|
|
93
93
|
"phone-flip": { label: "폰 플립", description: "전면/후면 카메라 플립입니다" },
|
|
94
94
|
// Location-studio extension (PR #2505 follow-up)
|