diff --git a/backend/apps/generation/migrations/0010_generationrecord_seed.py b/backend/apps/generation/migrations/0010_generationrecord_seed.py new file mode 100644 index 0000000..fd0beb9 --- /dev/null +++ b/backend/apps/generation/migrations/0010_generationrecord_seed.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.29 on 2026-03-22 14:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generation', '0009_generationrecord_is_favorited'), + ] + + operations = [ + migrations.AddField( + model_name='generationrecord', + name='seed', + field=models.BigIntegerField(default=-1, verbose_name='种子值'), + ), + ] diff --git a/backend/apps/generation/models.py b/backend/apps/generation/models.py index 72045ce..a4cf854 100644 --- a/backend/apps/generation/models.py +++ b/backend/apps/generation/models.py @@ -45,6 +45,7 @@ class GenerationRecord(models.Model): error_message = models.TextField(blank=True, default='', verbose_name='错误信息') reference_urls = models.JSONField(default=list, blank=True, verbose_name='参考素材信息') is_favorited = models.BooleanField(default=False, verbose_name='已收藏') + seed = models.BigIntegerField(default=-1, verbose_name='种子值') created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间') class Meta: diff --git a/backend/apps/generation/views.py b/backend/apps/generation/views.py index 355c2b2..c689e5d 100644 --- a/backend/apps/generation/views.py +++ b/backend/apps/generation/views.py @@ -162,6 +162,7 @@ def video_generate_view(request): model = serializer.validated_data['model'] aspect_ratio = serializer.validated_data['aspect_ratio'] search_mode = request.data.get('search_mode', 'off') + seed = _safe_int(request.data.get('seed', -1), -1) # ── 预估 token 和费用 ── config = QuotaConfig.objects.get_or_create(pk=1)[0] @@ -329,6 +330,7 @@ def video_generate_view(request): cost_amount=0, base_cost_amount=0, reference_urls=reference_snapshots, + seed=seed, ) locked_team.frozen_amount = F('frozen_amount') + estimated_cost @@ -346,6 +348,7 @@ def video_generate_view(request): aspect_ratio=aspect_ratio, duration=duration, search_mode=search_mode, + seed=seed, ) ark_task_id = ark_response.get('id', '') record.ark_task_id = ark_task_id @@ -479,6 +482,11 @@ def video_task_detail_view(request, task_id): new_status = map_status(ark_resp.get('status', '')) record.status = new_status + # 保存火山返回的实际 seed 值 + returned_seed = ark_resp.get('seed') + if returned_seed is not None: + record.seed = returned_seed + if new_status == 'completed': video_url = extract_video_url(ark_resp) if video_url: @@ -513,7 +521,7 @@ def video_task_detail_view(request, task_id): # Seedance 未计费,释放冻结 _release_freeze(record) - record.save(update_fields=['status', 'result_url', 'error_message']) + record.save(update_fields=['status', 'result_url', 'error_message', 'seed']) except Exception as e: logger.exception('AirDrama API query failed for %s', ark_task_id) @@ -541,6 +549,7 @@ def _serialize_task(record): 'error_message': d.get('error_message', ''), 'reference_urls': d.get('reference_urls') or [], 'is_favorited': record.is_favorited, + 'seed': record.seed, 'created_at': record.created_at.isoformat(), } diff --git a/backend/utils/airdrama_client.py b/backend/utils/airdrama_client.py index 4bcef4c..60756c3 100644 --- a/backend/utils/airdrama_client.py +++ b/backend/utils/airdrama_client.py @@ -77,7 +77,7 @@ def _headers(): def create_task(prompt, model, content_items, aspect_ratio, duration, - generate_audio=True, search_mode='off'): + generate_audio=True, search_mode='off', seed=-1): """Create a video generation task. Args: @@ -106,6 +106,7 @@ def create_task(prompt, model, content_items, aspect_ratio, duration, 'ratio': aspect_ratio, 'duration': duration, 'watermark': False, + 'seed': seed, } if search_mode and search_mode != 'off': diff --git a/web/src/components/GenerationCard.tsx b/web/src/components/GenerationCard.tsx index 4c40f83..8d16575 100644 --- a/web/src/components/GenerationCard.tsx +++ b/web/src/components/GenerationCard.tsx @@ -383,6 +383,12 @@ export function GenerationCard({ task, onOpenDetail }: Props) { )} + {(task.seed ?? -1) > 0 && ( +
+ 种子值 + {task.seed} +
+ )} )} diff --git a/web/src/components/InputBar.module.css b/web/src/components/InputBar.module.css index 9372f29..605cfbf 100644 --- a/web/src/components/InputBar.module.css +++ b/web/src/components/InputBar.module.css @@ -1,3 +1,10 @@ +/* Hide number input spinners */ +:global(.hide-spin::-webkit-outer-spin-button), +:global(.hide-spin::-webkit-inner-spin-button) { + -webkit-appearance: none; + margin: 0; +} + .wrapper { width: 100%; padding: 8px 16px 20px; diff --git a/web/src/components/InputBar.tsx b/web/src/components/InputBar.tsx index 6a4c0a9..472166a 100644 --- a/web/src/components/InputBar.tsx +++ b/web/src/components/InputBar.tsx @@ -76,6 +76,10 @@ export function InputBar() { const [assetModalOpen, setAssetModalOpen] = useState(false); const searchMode = useInputBarStore((s) => s.searchMode); const setSearchMode = useInputBarStore((s) => s.setSearchMode); + const seed = useInputBarStore((s) => s.seed); + const seedEnabled = useInputBarStore((s) => s.seedEnabled); + const setSeed = useInputBarStore((s) => s.setSeed); + const setSeedEnabled = useInputBarStore((s) => s.setSeedEnabled); const references = useInputBarStore((s) => s.references); const editorHtml = useInputBarStore((s) => s.editorHtml); const firstFrame = useInputBarStore((s) => s.firstFrame); @@ -118,6 +122,18 @@ export function InputBar() { > 联网搜索 +
¥{(task.costAmount ?? 0).toFixed(2)} )} + {(task.seed ?? -1) > 0 && ( + <> + + 种子值: {task.seed} + + )}
diff --git a/web/src/components/VideoGenerationPage.tsx b/web/src/components/VideoGenerationPage.tsx index f6f55d6..333e770 100644 --- a/web/src/components/VideoGenerationPage.tsx +++ b/web/src/components/VideoGenerationPage.tsx @@ -124,7 +124,7 @@ export function VideoGenerationPage() {
{tasks.length === 0 ? (
-

在下方输入提示词,开始创作 AI 视频

+

Every frame was once just air.

) : (
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 53a6cd6..bf4d1bd 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -136,6 +136,7 @@ export const videoApi = { duration: number; references: { url: string; type: string; role: string; label: string; thumb_url?: string }[]; search_mode?: string; + seed?: number; }) => api.post<{ task_id: string; diff --git a/web/src/store/generation.ts b/web/src/store/generation.ts index d4158c9..d2aedfd 100644 --- a/web/src/store/generation.ts +++ b/web/src/store/generation.ts @@ -106,6 +106,7 @@ function backendToFrontend(bt: BackendTask): GenerationTask { tokensConsumed: bt.tokens_consumed || 0, costAmount: bt.cost_amount || 0, isFavorited: bt.is_favorited || false, + seed: bt.seed ?? -1, }; } @@ -168,6 +169,7 @@ function startPolling(taskId: string, frontendId: string) { errorMessage: mapErrorMessage(data.error_message) || t.errorMessage, tokensConsumed: data.tokens_consumed ?? t.tokensConsumed, costAmount: data.cost_amount ?? t.costAmount, + seed: data.seed ?? t.seed, } : t ), @@ -373,6 +375,7 @@ export const useGenerationStore = create((set, get) => ({ progress: 0, createdAt: Date.now(), isFavorited: false, + seed: input.seed ?? -1, }; set((s) => ({ tasks: [...s.tasks, placeholderTask] })); @@ -450,6 +453,7 @@ export const useGenerationStore = create((set, get) => ({ duration: input.duration, references: uploadedRefs, search_mode: input.searchMode || 'off', + seed: input.seed ?? -1, }); // Update task with real backend IDs @@ -552,6 +556,8 @@ export const useGenerationStore = create((set, get) => ({ label: r.label, tosUrl: r.previewUrl, })); + const taskSeed = task.seed ?? -1; + const currentSeedEnabled = useInputBarStore.getState().seedEnabled; useInputBarStore.setState({ prompt: task.prompt, editorHtml: task.editorHtml || task.prompt, @@ -559,6 +565,8 @@ export const useGenerationStore = create((set, get) => ({ duration: task.duration, references, assetMentions: task.assetMentions || [], + // 如果 seed 开关打开且 task 有有效 seed,填入;否则不动 + ...(currentSeedEnabled && taskSeed > 0 ? { seed: taskSeed } : {}), }); } else { // Keyframe mode: restore firstFrame and lastFrame diff --git a/web/src/store/inputBar.ts b/web/src/store/inputBar.ts index 354752f..20e8582 100644 --- a/web/src/store/inputBar.ts +++ b/web/src/store/inputBar.ts @@ -60,6 +60,12 @@ interface InputBarState { searchMode: 'smart' | 'off'; setSearchMode: (mode: 'smart' | 'off') => void; + // Seed (种子值) + seed: number; + seedEnabled: boolean; + setSeed: (seed: number) => void; + setSeedEnabled: (enabled: boolean) => void; + // Asset mentions (for reEdit/regenerate to pass asset data to PromptInput rebuild) assetMentions: { groupId: string; label: string; thumbUrl: string }[]; @@ -212,6 +218,11 @@ export const useInputBarStore = create((set, get) => ({ searchMode: 'off', setSearchMode: (searchMode) => set({ searchMode }), + seed: -1, + seedEnabled: false, + setSeed: (seed) => set({ seed }), + setSeedEnabled: (seedEnabled) => set({ seedEnabled, seed: -1 }), + assetMentions: [], insertAtTrigger: 0, diff --git a/web/src/types/index.ts b/web/src/types/index.ts index dc58c50..3bb8aad 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -50,6 +50,7 @@ export interface GenerationTask { tokensConsumed?: number; costAmount?: number; isFavorited?: boolean; + seed?: number; } export interface BackendTask { @@ -70,6 +71,7 @@ export interface BackendTask { error_message: string; reference_urls: { url: string; type: string; role: string; label: string }[]; is_favorited: boolean; + seed: number; created_at: string; }