Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/guides/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ For a self-hosted chat service, allow at least 8192 tokens of context: the
application sends `max_tokens: 8192`, so a smaller `--max-model-len` (for
example vLLM's 4096 default) rejects every request with HTTP 400.

For vLLM-Omni video connections, enter fractional seconds directly, for example `6.583333`. The CLI accepts `opendirector video create --duration 6.583333` together with the other required options. Read `opendirector video create --help` and `opendirector model list` first; `inputRules.fractionalDuration` identifies support, and each output's duration list defines the allowed range. H3 can align output to its supported frame count, so inspect the actual result duration before assembling segments.

For detailed video settings, see [Video model capability configuration (Chinese)](video-model-capabilities.md). When no explicit output specification is provided, use **Auto** or **Service default duration**.

Automatic region selection in images and video matting require a model with segmentation capabilities. Image generation and editing still require the corresponding generation or editing model.
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/user-guide.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ HTTP 400。

在设置的“通用”页中可切换主题、调整去除水印选项。水印设置仅影响后续生成的内容,实际效果取决于模型服务是否支持。

vLLM-Omni 视频连接支持直接输入小数秒,例如 `6.583333`。CLI 可使用 `opendirector video create --duration 6.583333` 并填写其余必需参数。先运行 `opendirector video create --help` 和 `opendirector model list`;`inputRules.fractionalDuration` 表示支持小数秒,各输出规格的时长列表定义允许范围。H3 可能按模型支持的帧数对齐,拼接分段前应核对结果的实际时长。

## 制作第一支短片

建议先完成一个镜头的生成与导出,再扩展到多段素材。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from opendirector.domain.input_rules import input_rules
from opendirector.domain.minimax_speech import is_minimax_speech
from opendirector.domain.projects import ProjectError
from opendirector.domain.rule_facts import duration_matches
from opendirector.domain.video_output import apply_video_resolution
from opendirector.ports.gateway import gateway
from opendirector.integrations.http_settings import MODEL_JOB_CONTEXT
Expand Down Expand Up @@ -212,7 +213,7 @@ def _parameters(self, spec, config, node, records):
if node.get('videoMode') == 'continuation':
raise ProjectError('接续沿用上一段视频的画幅,请将比例设为自动。', 422)
caps = rules.get('videoCapabilities') or {}
choices = [item for item in caps.get('outputs', []) if seconds is None or seconds in item['durations']]
choices = [item for item in caps.get('outputs', []) if seconds is None or duration_matches(seconds, item['durations'], rules.get('fractionalDuration', False))]
if not choices:
raise ProjectError('指定视频比例前,请在模型接入中配置输出规格。', 422)
preferred = next((item for item in choices if item['resolution'] == caps.get('defaultResolution')), choices[0])
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/opendirector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ def add_material(ctx: typer.Context, asset: Annotated[UUID, typer.Option(help="R
def video_create(ctx: typer.Context,
model: Annotated[str, typer.Option(help="Video model configuration UUID or unique exact model name from opendirector model list.")],
prompt: Annotated[str, typer.Option(help="Video generation prompt.")],
duration: Annotated[float, typer.Option(min=0, help="Video duration in seconds.")],
duration: Annotated[float, typer.Option(min=0, help="Video duration in seconds, e.g. 6.583333. Fractional values require model inputRules.fractionalDuration; inspect model list for supported ranges. H3 may align the generated frame count.")],
x: Annotated[float, typer.Option(help="Canvas X coordinate. Choose using the current item positions and sizes.")],
y: Annotated[float, typer.Option(help="Canvas Y coordinate. Choose using the current item positions and sizes.")],
first_frame: Annotated[UUID | None, typer.Option(help="Source image canvas item for first-frame mode.")] = None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
FASTH3_MEDIA_REFERENCE_LIMIT, FRAME_IMAGE_LIMIT, LYRICS_MAX_LENGTH, MAX_REFERENCES,
MUSIC_SECONDS_INITIAL, MUSIC_SECONDS_MAX, MUSIC_SECONDS_MIN, PROMPT_MAX_LENGTH,
REFERENCE_MEDIA_SECONDS, STORED_VOICE_PATTERN, TEXT_DEFAULT_MAX_TOKENS, TEXT_MAX_TOKENS,
effective_video_mode, generate_audio_options,
duration_matches, effective_video_mode, generate_audio_options,
)
from .assets import AssetRef, MediaMetadata
from .base import CanvasItemVersion, Contract, Digest, GenerationKind, NonNegative, ObjectVersion, PositiveInt, ProjectEntity, Revision, digest, unique
Expand All @@ -36,7 +36,7 @@ class ImageParameters(Contract):
class VideoParameters(Contract):
generateAudio: bool = Field(default=True, description='Include audio; value must be allowed by model inputRules.generateAudioOptions.')
videoMode: VideoMode = Field(default='first_frame', description='Video generation input mode.')
durationSeconds: Annotated[float, Field(strict=True, ge=1, le=60)] | None = Field(default=None, description='Duration in seconds.')
durationSeconds: Annotated[float, Field(strict=True, ge=1, le=60)] | None = Field(default=None, description='Duration in seconds; fractional values require model inputRules.fractionalDuration. Generated frames may be aligned by the model.')
aspectRatio: str | None = Field(default=None, pattern=r'^[1-9]\d?:[1-9]\d?$', description='Output width-to-height ratio.')
resolution: str = Field(default='auto', pattern=r'^(?:auto|[A-Z0-9][A-Z0-9_.-]{0,23})$', description='Requested output resolution.')

Expand Down Expand Up @@ -490,5 +490,5 @@ def validate_generation(spec: GenerationSpec, model: ModelConfigPublic, referenc
caps = rules.videoCapabilities
if caps and params.durationSeconds is not None:
choices = [output for output in caps.outputs if params.resolution == 'auto' or output.resolution == params.resolution]
if not any(params.durationSeconds in output.durations for output in choices):
if not any(duration_matches(params.durationSeconds, output.durations, rules.fractionalDuration) for output in choices):
raise ValueError('当前时长与分辨率组合不受支持。')
1 change: 1 addition & 0 deletions packages/core/src/opendirector/domain/creation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class VoiceLibraryCapabilities(Contract):


class InputRules(Contract):
fractionalDuration: bool = Field(default=False, exclude_if=lambda value: not value, description="Accept fractional video seconds within each output duration range.")
profile: str = Field(min_length=1, max_length=100, description='Model capability profile.')
voiceLibrary: VoiceLibraryCapabilities | None = Field(default=None, exclude_if=lambda value: value is None,
description='Supported voice registry and adapter requirements; omitted for unsupported models.')
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/opendirector/domain/rule_facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ def aspect_ratio_choices(spec):
return ['auto']


def duration_matches(seconds: float, durations: list[int] | tuple[int, ...], fractional: bool = False) -> bool:
if fractional:
return bool(durations) and min(durations) <= seconds <= max(durations)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

区间判定只取 min/max,和 H3 实际可达帧不一致:h3_frames(vllm_omni.py)把阶数 steps 夹在 [6,20],对应 107–345 帧、24fps 即 4.458–14.375s;但这里按配置 durations 的 [min,max](H3 预设为 4–15)放行,于是端点 4s/15s 会分别被静默输出为 4.458s/14.375s,用户选区间端点时实际时长偏差最大。建议要么把可接受区间与实际可达帧对齐,要么在 UI 回显 h3_frames 对齐后的实际时长(目前仅靠用户指南提示「拼接前核对实际时长」)。

return seconds in durations


def resolution_choices(spec):
"""Sizes offered before submit. Callers report whether an image reference is connected."""
kind = spec['kind']
Expand All @@ -122,7 +128,7 @@ def resolution_choices(spec):
duration = spec.get('durationSeconds')
if outputs is not None and duration is not None:
choices = [label for label in choices
if any(item['resolution'] == label and duration in item['durations'] for item in outputs)]
if any(item['resolution'] == label and duration_matches(duration, item['durations'], spec.get('fractionalDuration', False)) for item in outputs)]
return ['auto', *choices]


Expand Down Expand Up @@ -336,7 +342,14 @@ def derivation_vectors():
return ["auto"];
}

export function durationMatches(seconds: number, durations: readonly number[], fractional = false): boolean {
return fractional
? durations.length > 0 && seconds >= Math.min(...durations) && seconds <= Math.max(...durations)
: durations.includes(seconds);
}

export function resolutionChoices(input: {
readonly fractionalDuration?: boolean;
readonly kind: string;
readonly videoMode?: string | null;
readonly aspectRatio?: string | null;
Expand Down Expand Up @@ -380,7 +393,7 @@ def derivation_vectors():
? choices.filter((label) =>
outputs.some(
(item) =>
item.resolution === label && item.durations.includes(duration),
item.resolution === label && durationMatches(duration, item.durations, input.fractionalDuration),
),
)
: choices;
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/opendirector/domain/video_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pydantic import BaseModel, ConfigDict, Field, StrictInt, model_validator

from opendirector.domain.projects import ProjectError
from opendirector.domain.rule_facts import duration_matches
from opendirector.domain.minimax_models import MINIMAX_MODELS, minimax_capabilities
from opendirector.domain.kling_models import kling_capabilities
from opendirector.domain.bigmodel_video import cogvideo_capabilities, validate_cogvideo_outputs
Expand Down Expand Up @@ -97,6 +98,8 @@ def capability_fields(config, rules):
validate_cogvideo_outputs(capabilities)
if rules['profile'] == 'kling-official':
validate_kling_outputs(capabilities, config['modelId'])
if config.get('kind') == 'video' and config.get('protocol') == 'vllm_omni':
rules['fractionalDuration'] = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

范围建议收紧到 H3。这里只要 kind=='video' 且 protocol=='vllm_omni' 就把 fractionalDuration 置真,于是 standard-video(通用 vLLM-Omni 视频)和 fasth3-omni 也会走区间判定。配合 rule_facts.duration_matches 的 min(durations) <= x <= max(durations),一个只声明离散时长的普通 vLLM-Omni 模型(例如 durations 为 [4,8])会接受并原样下发 4–8 之间任意小数,前端时长控件也会退化成自由滑杆;本 PR 的浏览器用例正是把稀疏时长 fixture 从 vllm_omni 改到 volcengine 才保住离散行为。若非确认所有受支持的 vllm_omni 视频模型都能处理任意秒数,建议改成 rules['profile'] in {'vllm-omni-h3','fasth3-omni'} 时才置真,或引入显式的每模型能力位。

rules['videoCapabilities'] = capabilities
rules['outputResolutions'] = [item['resolution'] for item in capabilities['outputs']] if capabilities else []
return rules
Expand Down Expand Up @@ -153,9 +156,10 @@ def check_video_config(kind, protocol, adapter, capabilities):

def validate_video_duration(config, seconds, resolution=None):
from opendirector.domain.input_rules import input_rules
capabilities = input_rules(config).get('videoCapabilities')
rules = input_rules(config)
capabilities = rules.get('videoCapabilities')
if capabilities:
choices = [item for item in capabilities['outputs']
if not resolution or resolution in {'auto', '自动'} or item['resolution'] == resolution.upper()]
if not any(seconds in item['durations'] for item in choices):
if not any(duration_matches(seconds, item['durations'], rules.get('fractionalDuration', False)) for item in choices):
raise ProjectError('当前模型的分辨率与时长组合不受支持,请重新选择;未提交生成。')
4 changes: 3 additions & 1 deletion packages/core/src/opendirector/integrations/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ async def generate(self, config, task, submitted, *, result_ready=None):
files.append(('input_references' if fast else 'input_reference', (meta['name'], data, image_type(data) if source_kind == 'image' else meta['type'])))
parameters = task['parameters']
form = {'model': config['modelId'], 'prompt': prepared['prompt']}
if 'duration' in parameters: form['seconds'] = format(parameters['duration'], 'g')
if 'duration' in parameters:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FastH3 的小数时长实际不会走这里设置的 form['seconds']:本块的写入会紧接着被 fast 分支的 form.update({'seconds': str(parameters.get('seconds','5')), ...}) 覆盖,而视频 task 参数里没有 'seconds' 键(只有 'duration',见 creation_generation.py 的 _parameters),因此 FastH3 恒定提交 seconds=5,真实时长只经 extra_params.duration(float,见第 191 行)下发。它是否生效取决于服务端是否读 extra_params.duration,PR 描述也承认该路径未验证;而 UI 已对 FastH3 打开小数输入。建议补一条真实 FastH3 的 wire/集成断言或明确标注此限制;第 184 行对 standard-video 有效,但对 fast 分支是死代码,可考虑仅在非 fast 时执行以更清晰。

seconds = parameters['duration']
form['seconds'] = str(int(seconds)) if float(seconds).is_integer() else str(seconds)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

form['seconds'] 从 format(seconds,'g')(6 位有效数字)改为 str(seconds) 后会输出 4.458333333333333 这类全精度串——这正是精确性所需(H3 的精确帧走 num_frames,不受影响),但标准 vLLM-Omni 服务端能否解析全精度小数尚未验证。建议确认后端对全精度浮点的接受度,必要时固定到某个足够精度。

if 'size' in parameters: form['size'] = parameters['size']
if fast:
form.update({
Expand Down
7 changes: 5 additions & 2 deletions src/backend/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { nodeByteSize } from "../canvas/uploadPolicy.ts";
import type { CanvasNode, GenerationSettings } from "../canvas/model.ts";
import {
RULE_FACTS,
durationMatches,
aspectRatioChoices,
effectiveVideoMode,
generateAudioOptions,
Expand Down Expand Up @@ -33,6 +34,7 @@ export interface VideoCapabilities {
}

export interface InputRules {
fractionalDuration?: boolean;
profile: string;
allowedKinds: string[];
maxImages: number;
Expand Down Expand Up @@ -369,6 +371,7 @@ export function backendResolutions(
): string[] {
const rules = config?.inputRules;
return resolutionChoices({
fractionalDuration: rules?.fractionalDuration,
kind: settings.mode,
videoMode: settings.videoMode,
aspectRatio: settings.aspectRatio,
Expand Down Expand Up @@ -483,7 +486,7 @@ export function serviceVideoSettings(
const next = { ...settings };
const caps = config.inputRules.videoCapabilities;
const durations = backendDurations(config);
if (durations.length && !durations.includes(next.durationSeconds))
if (durations.length && !durationMatches(next.durationSeconds, durations, config.inputRules.fractionalDuration))
next.durationSeconds = caps!.defaultDuration;
if (
next.videoMode === "continuation" &&
Expand Down Expand Up @@ -637,7 +640,7 @@ export function backendBlocker(
const references = backendReferences(settings, [...incoming, ...pinned], config);
if (settings.mode === "video") {
const durations = backendDurations(config);
if (durations.length && !durations.includes(settings.durationSeconds))
if (durations.length && !durationMatches(settings.durationSeconds, durations, rules.fractionalDuration))
return tNow("errors.rules.duration");
if (
rules.outputResolutions &&
Expand Down
14 changes: 9 additions & 5 deletions src/canvas/DurationControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export function DurationControl({
disabled,
onChange,
scale,
fractional = false,
}: {
fractional?: boolean;
values: readonly number[];
value: number;
disabled: boolean;
Expand Down Expand Up @@ -52,14 +54,14 @@ export function DurationControl({
const hint = (seconds: number) =>
seconds <= minimum || seconds >= maximum
? limit
: !values.includes(seconds)
: !fractional && !values.includes(seconds)
? t("canvasNodes.duration.unsupported", {
values: values.join(t("canvasNodes.duration.separator")),
})
: "";
const choose = (seconds: number) => {
if (!Number.isFinite(seconds) || !values.length || disabled) return;
const allowed = values.reduce((nearest, candidate) =>
const allowed = fractional ? Math.max(minimum, Math.min(maximum, seconds)) : values.reduce((nearest, candidate) =>
Math.abs(candidate - seconds) < Math.abs(nearest - seconds)
? candidate
: nearest,
Expand Down Expand Up @@ -97,7 +99,7 @@ export function DurationControl({
aria-label={t("canvasNodes.duration.aria")}
min={min}
max={max}
step={1}
step={fractional ? "any" : 1}
disabled={disabled || values.length <= 1}
value={Math.max(min, Math.min(max, value))}
aria-valuetext={t("canvasNodes.duration.value", { value })}
Expand All @@ -109,6 +111,7 @@ export function DurationControl({
if (event.buttons === 1) pointerHint(event);
}}
onKeyDown={(event) => {
if (fractional) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

小数时长下有几处小问题:(1) 第 114 行 if (fractional) return; 直接禁用了滑杆的 Home/End/方向键步进;(2) 第 103/155 行 disabled={disabled || values.length <= 1} 在单值配置下会连数字输入一起禁掉,用户无法录入小数;(3) 第 54–61 行 hint() 在取值恰为 min/max 时仍返回 limit 提示(此时其实合法)。建议:fractional 时保留键盘步进或统一交给数字输入;单值时仍允许数字输入(仅禁用滑杆);边界取值不再提示 limit。

const index = Math.max(0, values.indexOf(value));
const next =
event.key === "Home"
Expand Down Expand Up @@ -142,9 +145,10 @@ export function DurationControl({
<input
type="number"
aria-label={t("canvasNodes.duration.exactAria")}
title={fractional ? t("generation.duration.fractionalHint") : undefined}
min={minimum}
max={maximum}
step={1}
step={fractional ? "any" : 1}
aria-description={limit}
aria-describedby={notice ? noticeId : undefined}
value={draft}
Expand All @@ -154,7 +158,7 @@ export function DurationControl({
setDraft(next);
// Persist a complete valid number immediately, so dismissing the
// popover cannot discard it before a native blur is delivered.
if (next.trim() && values.includes(Number(next)))
if (next.trim() && Number.isFinite(Number(next)) && (values.includes(Number(next)) || fractional && Number(next) >= minimum && Number(next) <= maximum))
choose(Number(next));
}}
onBlur={commit}
Expand Down
1 change: 1 addition & 0 deletions src/canvas/NodeComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,7 @@ export function NodeComposer({
placement="above-anchor"
>
<DurationControl
fractional={!!config?.inputRules.fractionalDuration}
values={durationValues}
value={settings.durationSeconds}
disabled={running}
Expand Down
16 changes: 14 additions & 2 deletions src/contracts/creation.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6568,6 +6568,12 @@
"InputRules": {
"additionalProperties": false,
"properties": {
"fractionalDuration": {
"default": false,
"description": "Accept fractional video seconds within each output duration range.",
"title": "Fractionalduration",
"type": "boolean"
},
"profile": {
"description": "Model capability profile.",
"maxLength": 100,
Expand Down Expand Up @@ -14301,7 +14307,7 @@
}
],
"default": null,
"description": "Duration in seconds.",
"description": "Duration in seconds; fractional values require model inputRules.fractionalDuration. Generated frames may be aligned by the model.",
"title": "Durationseconds"
},
"aspectRatio": {
Expand Down Expand Up @@ -21932,6 +21938,12 @@
"InputRules": {
"additionalProperties": false,
"properties": {
"fractionalDuration": {
"default": false,
"description": "Accept fractional video seconds within each output duration range.",
"title": "Fractionalduration",
"type": "boolean"
},
"profile": {
"description": "Model capability profile.",
"maxLength": 100,
Expand Down Expand Up @@ -29961,7 +29973,7 @@
}
],
"default": null,
"description": "Duration in seconds.",
"description": "Duration in seconds; fractional values require model inputRules.fractionalDuration. Generated frames may be aligned by the model.",
"title": "Durationseconds"
},
"aspectRatio": {
Expand Down
2 changes: 2 additions & 0 deletions src/contracts/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ export interface ImageParametersInput extends ContractInput {
}

export interface InputRulesInput extends ContractInput {
readonly fractionalDuration?: boolean;
readonly profile: string;
readonly voiceLibrary?: VoiceLibraryCapabilitiesInput | null;
readonly maxPromptCharacters?: number | null;
Expand Down Expand Up @@ -2240,6 +2241,7 @@ export interface ImageParameters extends Contract {
}

export interface InputRules extends Contract {
readonly fractionalDuration?: boolean;
readonly profile: string;
readonly voiceLibrary?: VoiceLibraryCapabilities | null;
readonly maxPromptCharacters?: number | null;
Expand Down
9 changes: 8 additions & 1 deletion src/contracts/ruleFacts.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@ export function aspectRatioChoices(input: {
return ["auto"];
}

export function durationMatches(seconds: number, durations: readonly number[], fractional = false): boolean {
return fractional
? durations.length > 0 && seconds >= Math.min(...durations) && seconds <= Math.max(...durations)
: durations.includes(seconds);
}

export function resolutionChoices(input: {
readonly fractionalDuration?: boolean;
readonly kind: string;
readonly videoMode?: string | null;
readonly aspectRatio?: string | null;
Expand Down Expand Up @@ -120,7 +127,7 @@ export function resolutionChoices(input: {
? choices.filter((label) =>
outputs.some(
(item) =>
item.resolution === label && item.durations.includes(duration),
item.resolution === label && durationMatches(duration, item.durations, input.fractionalDuration),
),
)
: choices;
Expand Down
1 change: 1 addition & 0 deletions src/i18n/messages/en-US/generation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** Composer, generation state, model choice and generation rules. */
export const generation = {
"generation.duration.fractionalHint": "Fractional seconds are supported. H3 may align the output to its supported frame count.",
"generation.bigmodel.watermarkWaiver":
"BigModel requires a signed waiver to remove watermarks. Complete it in Personal Center → Security Management → Watermark Removal Management; otherwise outputs may still contain watermarks.",
"generation.model.unconfigured":
Expand Down
Loading
Loading