{footerControls}
diff --git a/frontend/apps/artcraft-webapp/src/pages/create-image/create-image.tsx b/frontend/apps/artcraft-webapp/src/pages/create-image/create-image.tsx
index 6a14e870ae..fe52b7a710 100644
--- a/frontend/apps/artcraft-webapp/src/pages/create-image/create-image.tsx
+++ b/frontend/apps/artcraft-webapp/src/pages/create-image/create-image.tsx
@@ -121,6 +121,9 @@ export default function CreateImage() {
return apiModels.find((m) => m.model === DEFAULT_MODEL_ID) ?? apiModels[0];
}, [apiModels, ui.selectedModelId]);
+ // Soft prompt limit from the API; undefined (no model / unset) = unlimited.
+ const maxPromptLength = selectedModel?.text_prompt_max_length ?? undefined;
+
const prompt = ui.prompt;
const setPrompt = useCallback((v: string) => setUi({ prompt: v }), [setUi]);
@@ -363,6 +366,13 @@ export default function CreateImage() {
}
if (!prompt.trim() || isGenerating || !selectedModel) return;
+ if (maxPromptLength !== undefined && prompt.length > maxPromptLength) {
+ toast.error(
+ `Prompt exceeds the ${maxPromptLength} character limit for this model`,
+ );
+ return;
+ }
+
setIsGenerating(true);
const batchId = startBatch(
prompt,
@@ -435,6 +445,7 @@ export default function CreateImage() {
prompt,
isGenerating,
selectedModel,
+ maxPromptLength,
numImages,
aspectRatio,
resolution,
@@ -618,6 +629,7 @@ export default function CreateImage() {
onSubmit={handleGenerate}
isSubmitting={isGenerating}
credits={estimatedCredits}
+ maxPromptLength={maxPromptLength}
placeholder="Describe what you want in the image..."
supportsImagePrompts={!!selectedModel?.image_refs_supported}
maxImagePromptCount={maxImageRefs}
diff --git a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx
index 304bef076f..90dd7b6562 100644
--- a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx
+++ b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx
@@ -62,6 +62,7 @@ import {
OMNI_GENERATE_OUTAGE_MESSAGE,
} from "@storyteller/omni-gen";
import {
+ effectivePromptMaxLength,
getCreatorIconPathForModelId,
getModelDescription,
getModelInfo,
@@ -273,6 +274,18 @@ export default function CreateVideo() {
const requiresImageInput = selectedModel?.text_to_video_supported === false;
+ // Soft prompt limit from the API; undefined = unlimited. Seedance waives
+ // the limit (Infinity) when the prompt contains Chinese characters: the
+ // server counts CJK as more than one unit, so a client-side character
+ // count would false-positive.
+ const maxPromptLength = selectedModel
+ ? effectivePromptMaxLength(
+ selectedModel.model,
+ selectedModel.text_prompt_max_length ?? undefined,
+ ui.prompt,
+ )
+ : undefined;
+
const prompt = ui.prompt;
const setPrompt = useCallback((v: string) => setUi({ prompt: v }), [setUi]);
@@ -877,6 +890,12 @@ export default function CreateVideo() {
});
return;
}
+ if (maxPromptLength !== undefined && prompt.length > maxPromptLength) {
+ toast.error(
+ `Prompt exceeds the ${maxPromptLength} character limit for this model`,
+ );
+ return;
+ }
console.log("[generate-video] starting", {
model: selectedModel.model,
numVideos,
@@ -1036,6 +1055,7 @@ export default function CreateVideo() {
needsImage,
isReferenceMode,
selectedModel,
+ maxPromptLength,
selectedSize,
numVideos,
effectiveDuration,
@@ -1364,6 +1384,7 @@ export default function CreateVideo() {
onSubmit={handleGenerate}
isSubmitting={isGenerating}
credits={estimatedCredits}
+ maxPromptLength={maxPromptLength}
placeholder="Describe the video you want to generate..."
supportsImagePrompts={supportsImagePrompts}
maxImagePromptCount={
diff --git a/frontend/apps/artcraft-webapp/src/styles.css b/frontend/apps/artcraft-webapp/src/styles.css
index f6c12154e0..ee9813fe57 100644
--- a/frontend/apps/artcraft-webapp/src/styles.css
+++ b/frontend/apps/artcraft-webapp/src/styles.css
@@ -271,6 +271,43 @@
background-color: rgba(255, 255, 255, 0.35);
}
+/* Hide the native resize grip (Chromium draws different grips for textareas
+ vs contenteditable divs) and draw one consistent grip on the wrapper so the
+ plain and @-mention prompt editors look identical. */
+.promptbox-scrollbar::-webkit-resizer {
+ background: transparent;
+ border: none;
+}
+
+.promptbox-resize-wrap {
+ position: relative;
+}
+
+.promptbox-resize-wrap::after {
+ content: "";
+ position: absolute;
+ bottom: 2px;
+ right: 2px;
+ width: 10px;
+ height: 10px;
+ pointer-events: none;
+ background: linear-gradient(
+ 135deg,
+ transparent 40%,
+ rgba(255, 255, 255, 0.5) 40%,
+ rgba(255, 255, 255, 0.5) 45%,
+ transparent 45%,
+ transparent 55%,
+ rgba(255, 255, 255, 0.5) 55%,
+ rgba(255, 255, 255, 0.5) 60%,
+ transparent 60%,
+ transparent 70%,
+ rgba(255, 255, 255, 0.5) 70%,
+ rgba(255, 255, 255, 0.5) 75%,
+ transparent 75%
+ );
+}
+
@keyframes fade-in-up {
from {
opacity: 0;
diff --git a/frontend/apps/artcraft-website/src/components/lightbox/LightboxDetails.tsx b/frontend/apps/artcraft-website/src/components/lightbox/LightboxDetails.tsx
index 40c99438d5..67f2606c23 100644
--- a/frontend/apps/artcraft-website/src/components/lightbox/LightboxDetails.tsx
+++ b/frontend/apps/artcraft-website/src/components/lightbox/LightboxDetails.tsx
@@ -25,7 +25,7 @@ import {
THUMBNAIL_SIZES,
} from "@storyteller/common";
import {
- getModelCreatorIcon,
+ getCreatorIconPathForModelId,
getModelDisplayName,
getProviderDisplayName,
getProviderIconByName,
@@ -297,7 +297,13 @@ export function LightboxDetails({
label="Model"
value={
<>
- {getModelCreatorIcon(promptData.modelType)}
+
{getModelDisplayName(promptData.modelType)}
diff --git a/frontend/apps/artcraft-website/src/components/prompt-box/PromptBox.tsx b/frontend/apps/artcraft-website/src/components/prompt-box/PromptBox.tsx
index 7dfde110d6..890ecab98b 100644
--- a/frontend/apps/artcraft-website/src/components/prompt-box/PromptBox.tsx
+++ b/frontend/apps/artcraft-website/src/components/prompt-box/PromptBox.tsx
@@ -109,6 +109,11 @@ interface PromptBoxProps {
// @-mention support (enables colored prompt overlay + autocomplete)
mentionItems?: MentionItem[];
+
+ // Soft prompt-length limit from the model API (`text_prompt_max_length`).
+ // Undefined = unlimited (no counter). The limit is not enforced here; the
+ // page's submit handler blocks generation when over.
+ maxPromptLength?: number;
}
export const PromptBox = forwardRef
(
@@ -139,6 +144,7 @@ export const PromptBox = forwardRef(
mediaReferenceRow,
modelSelector,
mentionItems,
+ maxPromptLength,
},
ref,
) => {
@@ -423,7 +429,7 @@ export const PromptBox = forwardRef(
)}
-
+
{hasMentionItems && mentionItems ? (
(
)}
>
)}
+
+ {maxPromptLength !== undefined && (
+ maxPromptLength
+ ? "text-red-500"
+ : "text-base-fg/40",
+ )}
+ >
+ {prompt.length} /{" "}
+ {isFinite(maxPromptLength) ? maxPromptLength : "∞"}
+
+ )}
diff --git a/frontend/apps/artcraft-website/src/pages/create-image/create-image.tsx b/frontend/apps/artcraft-website/src/pages/create-image/create-image.tsx
index 094bc3dcf9..167591e4e5 100644
--- a/frontend/apps/artcraft-website/src/pages/create-image/create-image.tsx
+++ b/frontend/apps/artcraft-website/src/pages/create-image/create-image.tsx
@@ -20,6 +20,7 @@ import {
} from "../../components/generation-gallery";
import { Lightbox } from "../../components/lightbox/lightbox";
import { AppDownloadCta } from "../../components/app-download-cta/AppDownloadCta";
+import { toast } from "../../components/toast/toast";
import { useCreateImageStore } from "./create-image-store";
import { enqueueImageGeneration, startPolling } from "./generate-image-api";
import { AspectRatioPicker } from "./components/AspectRatioPicker";
@@ -83,6 +84,9 @@ export default function CreateImage() {
return apiModels.find((m) => m.model === DEFAULT_MODEL_ID) ?? apiModels[0];
}, [apiModels, ui.selectedModelId]);
+ // Soft prompt limit from the API; undefined (no model / unset) = unlimited.
+ const maxPromptLength = selectedModel?.text_prompt_max_length ?? undefined;
+
const prompt = ui.prompt;
const setPrompt = useCallback((v: string) => setUi({ prompt: v }), [setUi]);
const aspectRatio = ui.aspectRatio;
@@ -278,6 +282,13 @@ export default function CreateImage() {
const handleGenerate = useCallback(async () => {
if (!prompt.trim() || isGenerating || !selectedModel) return;
+ if (maxPromptLength !== undefined && prompt.length > maxPromptLength) {
+ toast.error(
+ `Prompt exceeds the ${maxPromptLength} character limit for this model`,
+ );
+ return;
+ }
+
setIsGenerating(true);
const batchId = startBatch(
prompt,
@@ -337,6 +348,7 @@ export default function CreateImage() {
prompt,
isGenerating,
selectedModel,
+ maxPromptLength,
numImages,
aspectRatio,
resolution,
@@ -396,6 +408,7 @@ export default function CreateImage() {
onSubmit={handleGenerate}
isSubmitting={isGenerating}
credits={estimatedCredits}
+ maxPromptLength={maxPromptLength}
placeholder="Describe what you want in the image..."
supportsImagePrompts={!!selectedModel?.image_refs_supported}
maxImagePromptCount={maxImageRefs}
diff --git a/frontend/apps/artcraft-website/src/pages/create-video/create-video.tsx b/frontend/apps/artcraft-website/src/pages/create-video/create-video.tsx
index a8ed54c4bf..46f996f600 100644
--- a/frontend/apps/artcraft-website/src/pages/create-video/create-video.tsx
+++ b/frontend/apps/artcraft-website/src/pages/create-video/create-video.tsx
@@ -46,7 +46,11 @@ import {
import { GenerationCountPicker } from "../create-image/components/GenerationCountPicker";
import { useVideoCostEstimate } from "../../lib/cost-estimate-api";
import { useOmniGenVideoModels } from "@storyteller/omni-gen";
-import { getCreatorIconPathForModelId } from "@storyteller/model-list";
+import {
+ effectivePromptMaxLength,
+ getCreatorIconPathForModelId,
+} from "@storyteller/model-list";
+import { toast } from "../../components/toast/toast";
// ── Constants ────────────────────────────────────────────────────────────
@@ -182,6 +186,18 @@ export default function CreateVideo() {
return apiModels.find((m) => m.model === DEFAULT_MODEL_ID) ?? apiModels[0];
}, [apiModels, ui.selectedModelId]);
+ // Soft prompt limit from the API; undefined = unlimited. Seedance waives
+ // the limit (Infinity) when the prompt contains Chinese characters: the
+ // server counts CJK as more than one unit, so a client-side character
+ // count would false-positive.
+ const maxPromptLength = selectedModel
+ ? effectivePromptMaxLength(
+ selectedModel.model,
+ selectedModel.text_prompt_max_length ?? undefined,
+ ui.prompt,
+ )
+ : undefined;
+
const prompt = ui.prompt;
const setPrompt = useCallback((v: string) => setUi({ prompt: v }), [setUi]);
const selectedSize = ui.selectedSize;
@@ -717,6 +733,12 @@ export default function CreateVideo() {
});
return;
}
+ if (maxPromptLength !== undefined && prompt.length > maxPromptLength) {
+ toast.error(
+ `Prompt exceeds the ${maxPromptLength} character limit for this model`,
+ );
+ return;
+ }
console.log("[generate-video] starting", {
model: selectedModel.model,
numVideos,
@@ -852,6 +874,7 @@ export default function CreateVideo() {
needsImage,
isReferenceMode,
selectedModel,
+ maxPromptLength,
selectedSize,
numVideos,
duration,
@@ -935,6 +958,7 @@ export default function CreateVideo() {
onSubmit={handleGenerate}
isSubmitting={isGenerating || needsImage}
credits={estimatedCredits}
+ maxPromptLength={maxPromptLength}
placeholder="Describe the video you want to generate..."
supportsImagePrompts={supportsImagePrompts}
maxImagePromptCount={
diff --git a/frontend/apps/artcraft-website/src/styles.css b/frontend/apps/artcraft-website/src/styles.css
index 39f3d4ea17..c5ffa988af 100644
--- a/frontend/apps/artcraft-website/src/styles.css
+++ b/frontend/apps/artcraft-website/src/styles.css
@@ -120,6 +120,43 @@
background-color: rgba(255, 255, 255, 0.35);
}
+/* Hide the native resize grip (Chromium draws different grips for textareas
+ vs contenteditable divs) and draw one consistent grip on the wrapper so the
+ plain and @-mention prompt editors look identical. */
+.promptbox-scrollbar::-webkit-resizer {
+ background: transparent;
+ border: none;
+}
+
+.promptbox-resize-wrap {
+ position: relative;
+}
+
+.promptbox-resize-wrap::after {
+ content: "";
+ position: absolute;
+ bottom: 2px;
+ right: 2px;
+ width: 10px;
+ height: 10px;
+ pointer-events: none;
+ background: linear-gradient(
+ 135deg,
+ transparent 40%,
+ rgba(255, 255, 255, 0.5) 40%,
+ rgba(255, 255, 255, 0.5) 45%,
+ transparent 45%,
+ transparent 55%,
+ rgba(255, 255, 255, 0.5) 55%,
+ rgba(255, 255, 255, 0.5) 60%,
+ transparent 60%,
+ transparent 70%,
+ rgba(255, 255, 255, 0.5) 70%,
+ rgba(255, 255, 255, 0.5) 75%,
+ transparent 75%
+ );
+}
+
@keyframes fade-in-up {
from {
opacity: 0;
diff --git a/frontend/libs/api/src/lib/OmniGenApi.ts b/frontend/libs/api/src/lib/OmniGenApi.ts
index 491201f83f..caf9b2fa6a 100644
--- a/frontend/libs/api/src/lib/OmniGenApi.ts
+++ b/frontend/libs/api/src/lib/OmniGenApi.ts
@@ -77,6 +77,8 @@ export interface OmniGenVideoGenerateResponse {
export interface OmniGenImageModelInfo {
model: string;
+ // ModelCreator enum as a snake_case string, e.g. "bytedance", "google".
+ model_creator?: string | null;
is_disabled: boolean | null;
full_name: string | null;
extra_info_short?: string | null;
@@ -111,6 +113,8 @@ export interface OmniGenImageModelsResponse {
export interface OmniGenVideoModelInfo {
model: string;
+ // ModelCreator enum as a snake_case string, e.g. "bytedance", "open_ai".
+ model_creator?: string | null;
is_disabled: boolean | null;
full_name: string | null;
extra_info_short?: string | null;
diff --git a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx
index 7753c7f921..b5cf53e1c2 100644
--- a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx
+++ b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx
@@ -43,7 +43,7 @@ import {
isActionReminderOpen,
} from "@storyteller/ui-action-reminder-modal";
import {
- getModelCreatorIcon,
+ getCreatorIconPathForModelId,
getModelDisplayName,
getProviderDisplayName,
getProviderIconByName,
@@ -1327,7 +1327,11 @@ function InfoSection({
label="Model"
value={
<>
- {getModelCreatorIcon(modelType)}
+
{getModelDisplayName(modelType)}
>
}
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx
index ffc724f7db..45bff2e3a1 100644
--- a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx
+++ b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx
@@ -19,6 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import {
CommonResolution,
+ effectivePromptMaxLength,
SizeIconOption,
SizeOption,
VideoModel,
@@ -697,7 +698,12 @@ export const PromptBoxVideo = ({
mentionAnchorRef.current = null;
};
- const maxLen = selectedModel?.maxPromptLength ?? 1000;
+ const maxLen =
+ effectivePromptMaxLength(
+ selectedModel?.tauriId ?? "",
+ selectedModel?.maxPromptLength,
+ prompt,
+ ) ?? 1000;
const handleEnqueue = async () => {
if (!prompt.trim()) {
diff --git a/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts b/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts
index 26c9da3485..3643827aa4 100644
--- a/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts
+++ b/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts
@@ -2,165 +2,31 @@ import React from "react";
import { ModelCreator } from "./ModelCreator.js";
import { getCreatorIcon } from "./ModelCreatorIcons.js";
-// Map from model type strings to ModelCreator enum values
-export const MODEL_TYPE_TO_CREATOR: Record = {
- flux_1_dev: ModelCreator.BlackForestLabs,
- flux_1_schnell: ModelCreator.BlackForestLabs,
- flux_pro_1p1: ModelCreator.BlackForestLabs,
- flux_pro_1p1_ultra: ModelCreator.BlackForestLabs,
- flux_pro_kontext_max: ModelCreator.BlackForestLabs,
- flux_dev_juggernaut: ModelCreator.BlackForestLabs,
- flux_pro_1: ModelCreator.BlackForestLabs,
- // Aliases commonly returned by other services
- flux_pro_1_1: ModelCreator.BlackForestLabs,
- flux_pro_1_1_ultra: ModelCreator.BlackForestLabs,
- gpt_image_1: ModelCreator.OpenAi,
- gpt_image_1p5: ModelCreator.OpenAi,
- gpt_image_2: ModelCreator.OpenAi,
- // Kling — p-style IDs
- kling_1p6_pro: ModelCreator.Kling,
- kling_2p1_pro: ModelCreator.Kling,
- kling_2p1_master: ModelCreator.Kling,
- kling_2p5_turbo_pro: ModelCreator.Kling,
- kling_2p6_pro: ModelCreator.Kling,
- kling_3p0_standard: ModelCreator.Kling,
- kling_3p0_pro: ModelCreator.Kling,
- // Kling — dot-normalized aliases (backend sends e.g. "kling_1.6_pro", normalizeModelKey converts . → _)
- kling_1_6_pro: ModelCreator.Kling,
- kling_2_1_pro: ModelCreator.Kling,
- kling_2_1_master: ModelCreator.Kling,
- // Seedance — p-style IDs
- seedance_1p0_lite: ModelCreator.Bytedance,
- seedance_1p5_pro: ModelCreator.Bytedance,
- seedance_2p0: ModelCreator.Bytedance,
- seedance_2p0_fast: ModelCreator.Bytedance,
- seedance_2p0_bp: ModelCreator.Bytedance,
- seedance_2p0_bp_fast: ModelCreator.Bytedance,
- seedance_2p0_u: ModelCreator.Bytedance,
- seedance_2p0_u_fast: ModelCreator.Bytedance,
- seedance_2p0_bpu: ModelCreator.Bytedance,
- seedance_2p0_bpu_fast: ModelCreator.Bytedance,
- // Seedance — dot-normalized aliases
- seedance_1_0_lite: ModelCreator.Bytedance,
- // Seedream
- seedream_4: ModelCreator.Bytedance,
- seedream_4p5: ModelCreator.Bytedance,
- seedream_5_lite: ModelCreator.Bytedance,
- // Sora
- sora_2: ModelCreator.OpenAi,
- sora_2_pro: ModelCreator.OpenAi,
- // Veo
- veo_2: ModelCreator.Google,
- veo_3: ModelCreator.Google,
- veo_3_fast: ModelCreator.Google,
- veo_3p1: ModelCreator.Google,
- veo_3p1_fast: ModelCreator.Google,
- gemini_25_flash: ModelCreator.Google,
- nano_banana: ModelCreator.Google,
- nano_banana_2: ModelCreator.Google,
- nano_banana_pro: ModelCreator.Google,
- recraft_3: ModelCreator.Recraft,
- // Hunyuan — p-style IDs
- hunyuan_3d: ModelCreator.Tencent,
- hunyuan_3d_2p0: ModelCreator.Tencent,
- hunyuan_3d_2p1: ModelCreator.Tencent,
- hunyuan_3d_2: ModelCreator.Tencent,
- hunyuan_3d_3: ModelCreator.Tencent,
- // Hunyuan — dot-normalized aliases
- hunyuan_3d_2_0: ModelCreator.Tencent,
- hunyuan_3d_2_1: ModelCreator.Tencent,
- worldlabs_gaussian: ModelCreator.WorldLabs,
- marble_0p1_mini: ModelCreator.WorldLabs,
- marble_0p1_plus: ModelCreator.WorldLabs,
- grok_image: ModelCreator.Grok,
- grok_imagine_image: ModelCreator.Grok,
- grok_imagine_image_q: ModelCreator.Grok,
- grok_video: ModelCreator.Grok,
- grok_imagine_video: ModelCreator.Grok,
- grok_imagine_video_1p5: ModelCreator.Grok,
- midjourney: ModelCreator.Midjourney,
- midjourney_v6: ModelCreator.Midjourney,
- midjourney_v6p1: ModelCreator.Midjourney,
- midjourney_v6p1_raw: ModelCreator.Midjourney,
- midjourney_v7: ModelCreator.Midjourney,
- midjourney_v7_raw: ModelCreator.Midjourney,
- midjourney_v7_draft_raw: ModelCreator.Midjourney,
- // Underscore-numbered IDs the backend actually sends for current MJ models
- midjourney_7: ModelCreator.Midjourney,
- midjourney_7_niji: ModelCreator.Midjourney,
- midjourney_8: ModelCreator.Midjourney,
- // Angles
- flux_2_lora_angles: ModelCreator.BlackForestLabs,
- qwen_edit_2511_angles: ModelCreator.Alibaba,
- // Happy Horse (Alibaba wanvideo)
- happy_horse_1p0: ModelCreator.Alibaba,
- // Beeble (Background Change / VFX)
- switch_x: ModelCreator.Beeble,
-};
-
-// Get creator icon for a model type
+// Normalize a model id/type key for lookups (lowercase, dots → underscores)
const normalizeModelKey = (modelType: string): string =>
modelType.toLowerCase().replace(/\./g, "_").trim();
-export const getModelCreatorIcon = (
- modelType: string,
-): React.ReactNode | null => {
- const creator = MODEL_TYPE_TO_CREATOR[normalizeModelKey(modelType)];
- if (!creator) return null;
- return getCreatorIcon(creator, "h-4 w-4 invert");
-};
+/** Matches all seedance_* variants (video-only; image models are seedream_*). */
+export const isSeedanceVideoModelId = (modelId: string): boolean =>
+ normalizeModelKey(modelId).startsWith("seedance");
-// Get creator name for display
-export const getModelCreatorName = (modelType: string): string | null => {
- const creator = MODEL_TYPE_TO_CREATOR[normalizeModelKey(modelType)];
+const CHINESE_CHARACTERS_REGEX = /\p{Script=Han}/u;
- // Convert enum value to display name
- switch (creator) {
- case ModelCreator.BlackForestLabs:
- return "Black Forest Labs";
- case ModelCreator.OpenAi:
- return "OpenAI";
- case ModelCreator.Kling:
- return "Kling AI";
- case ModelCreator.Bytedance:
- return "ByteDance";
- case ModelCreator.Google:
- return "Google";
- case ModelCreator.Midjourney:
- return "Midjourney";
- case ModelCreator.Stability:
- return "Stability AI";
- case ModelCreator.Runway:
- return "Runway";
- case ModelCreator.Hailuo:
- return "Hailuo";
- case ModelCreator.Recraft:
- return "Recraft";
- case ModelCreator.Tencent:
- return "Tencent";
- case ModelCreator.Alibaba:
- return "Alibaba";
- case ModelCreator.Vidu:
- return "Vidu";
- case ModelCreator.Fal:
- return "Fal";
- case ModelCreator.Replicate:
- return "Replicate";
- case ModelCreator.TensorArt:
- return "TensorArt";
- case ModelCreator.OpenArt:
- return "OpenArt";
- case ModelCreator.Higgsfield:
- return "Higgsfield";
- case ModelCreator.Krea:
- return "Krea";
- case ModelCreator.Grok:
- return "Grok";
- case ModelCreator.WorldLabs:
- return "World Labs";
- default:
- return creator;
+/**
+ * Effective soft prompt-length limit for a model. Normally the model's
+ * `text_prompt_max_length`, but seedance waives the limit (Infinity) when the
+ * prompt contains Chinese characters: the server counts CJK characters as
+ * more than one unit, so a client-side character count would false-positive.
+ */
+export const effectivePromptMaxLength = (
+ modelId: string,
+ modelMaxLength: number | undefined,
+ prompt: string,
+): number | undefined => {
+ if (isSeedanceVideoModelId(modelId) && CHINESE_CHARACTERS_REGEX.test(prompt)) {
+ return Infinity;
}
+ return modelMaxLength;
};
// Convert model type string to human-readable display name. An API-provided