{hint}
diff --git a/studio-app/src/StoryboardConnected.jsx b/studio-app/src/StoryboardConnected.jsx index 716c7b0..211a824 100644 --- a/studio-app/src/StoryboardConnected.jsx +++ b/studio-app/src/StoryboardConnected.jsx @@ -603,6 +603,7 @@ export default function StoryboardConnected({ onLegacyGuide, onCanvas, onNewScen readTakeReview: (shot, artifact) => cloud.readTakeReview(shot, artifact), readTakeReviewSummaries: (shot) => cloud.readTakeReviewSummaries(shot), readInkySuggestions: cloud.readInkySuggestions && (() => cloud.readInkySuggestions()), + readShotReadiness: cloud.readShotReadiness && (() => cloud.readShotReadiness()), ignoreInkySuggestion: (id) => cloud.ignoreInkySuggestion?.(id), saveTakeReview: (shot, artifact, body, key) => cloud.saveTakeReview(shot, artifact, body, key), readCopies: () => cloud.workbenchCopies(), diff --git a/studio-app/src/cloud-controller.js b/studio-app/src/cloud-controller.js index d844876..f6eab64 100644 --- a/studio-app/src/cloud-controller.js +++ b/studio-app/src/cloud-controller.js @@ -1847,6 +1847,13 @@ export function createCloudController({ if (!Array.isArray(value?.items)) throw Error('inky.unavailable'); return value; } + async function readShotReadiness() { + requireCloud(); + const ctx = context(); + const value = await api.request('/v1/inky/readiness' + inkyQuery(ctx)); + guard(ctx); + return value; + } async function ignoreInkySuggestion(id) { requireCloud(); const ctx = context(); @@ -2068,6 +2075,7 @@ export function createCloudController({ readTakeReview: (shotId, artifactId) => readTakeReview(shotId, artifactId), readTakeReviewSummaries: (shotId) => readTakeReviewSummaries(shotId), readInkySuggestions: () => readInkySuggestions(), + readShotReadiness: () => readShotReadiness(), ignoreInkySuggestion: (id) => ignoreInkySuggestion(id), saveTakeReview: (shotId, artifactId, body, key) => saveTakeReview(shotId, artifactId, body, key), diff --git a/studio-app/src/i18n/legacyCatalogs.js b/studio-app/src/i18n/legacyCatalogs.js index fd4a83a..32ee190 100644 --- a/studio-app/src/i18n/legacyCatalogs.js +++ b/studio-app/src/i18n/legacyCatalogs.js @@ -705,6 +705,10 @@ export const legacyCatalogs = { 'The {slot} image for “{character} / {look}” is missing. Upload it again or explicitly defer it.', 'projection.missingLook': 'The saved look for “{name}” no longer exists. Choose another.', 'projection.countLimit': 'Current model/API count limit ({kind}: {max}, total: {total})', + 'hint.characterPicture': + '{names} has no picture yet. Add one, or a first frame, or the person may look different in every shot.', + 'hint.speechUnspecified': + 'No spoken lines yet. Write what is said in quotes, or say “no dialogue”, so the voice does not invent words.', 'hint.focusRefs': 'Lots of references, or reference videos, may only be partly used. A few focused references usually work better.', 'hint.writeLines': @@ -2107,6 +2111,10 @@ export const legacyCatalogs = { 'legacy.lookImageMissing': '「{character} / {look}」的{slot}图缺失;补传或明确改为稍后补充。', 'projection.missingLook': '保存的「{name}」造型不存在,请重新选择。', 'projection.countLimit': '当前模型/API 数量上限({kind} {max},总计 {total})', + 'hint.characterPicture': + '{names} 还没有图片。加一张图或一个首帧,否则这个人物在每个镜头里可能长得不一样。', + 'hint.speechUnspecified': + '还没有写台词。用引号写出要说的话,或写明“无对白”,否则声音可能会自己编内容。', 'hint.focusRefs': '参考素材很多或有参考视频时,模型可能只用上一部分。少而精的参考通常效果更好。', 'hint.writeLines': diff --git a/studio-app/src/shot-readiness.js b/studio-app/src/shot-readiness.js new file mode 100644 index 0000000..0a97cd5 --- /dev/null +++ b/studio-app/src/shot-readiness.js @@ -0,0 +1,22 @@ +// What a shot is missing, from GET /v1/inky/readiness. Advice for a click: the user can +// still generate. +import { useQuery } from '@tanstack/react-query'; +import { useStoryboardUI } from './storyboard-ui-context.jsx'; +import { readinessHints } from './storyboard-domain.js'; + +export function useShotReadiness(shot, t) { + const { state, adapter } = useStoryboardUI(); + const read = adapter?.readShotReadiness; + const saved = state.shots.reduce((sum, row) => sum + (row.entity?.version || 0), 0); + const query = useQuery({ + queryKey: ['shot-readiness', state.contextId, state.project.id, saved], + queryFn: () => read(), + enabled: !!read && state.workspaceMode === 'cloud', + staleTime: 30000, + placeholderData: (previous) => previous, + // Not deployed yet: no hints rather than retries. + retry: false, + }); + const item = query.data?.items?.find((row) => row.shot_id === shot?.id); + return readinessHints(item, shot?.entity?.version, t); +} diff --git a/studio-app/src/storyboard-domain.js b/studio-app/src/storyboard-domain.js index d6e289e..98025b8 100644 --- a/studio-app/src/storyboard-domain.js +++ b/studio-app/src/storyboard-domain.js @@ -605,6 +605,17 @@ export function submitHints(shot, items, t) { return hints; } +// Only findings for the version on screen count, so a save clears them. +export function readinessHints(item, version, t) { + if (!item || item.base_version !== version) return []; + return (item.findings || []).flatMap((finding) => { + if (finding.code === 'character_picture_missing') + return [t('hint.characterPicture', { names: (finding.names || []).join(', ') })]; + if (finding.code === 'speech_unspecified') return [t('hint.speechUnspecified')]; + return []; + }); +} + function limitsLabel(limits, t) { if (!limits) return '能力尚未读取'; const known = (value) => (Number.isFinite(value) ? value : '—'); diff --git a/studio-app/src/storyboard-domain.test.js b/studio-app/src/storyboard-domain.test.js index 78e0c30..b4ff4e9 100644 --- a/studio-app/src/storyboard-domain.test.js +++ b/studio-app/src/storyboard-domain.test.js @@ -730,3 +730,22 @@ test('an enhance job joins the shot takes as an enhanced copy; the original stay ), ); }); + +test('readiness hints follow the version on screen and skip what the editor already shows', async () => { + const { readinessHints } = await import('./storyboard-domain.js'); + const t = (key, values) => key + (values?.names ? ':' + values.names : ''); + const item = { + base_version: 3, + findings: [ + { code: 'action_missing' }, + { code: 'character_picture_missing', names: ['Mia', 'Tom'] }, + { code: 'speech_unspecified' }, + ], + }; + assert.deepEqual(readinessHints(item, 3, t), [ + 'hint.characterPicture:Mia, Tom', + 'hint.speechUnspecified', + ]); + assert.deepEqual(readinessHints(item, 4, t), []); + assert.deepEqual(readinessHints(undefined, 3, t), []); +});