Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
!studio-app/src/AccountPassword.jsx
!studio-app/src/AccountGoogleLink.jsx
!studio-app/src/monitoring.js
!studio-app/src/shot-readiness.js
!studio-app/src/monitoring.test.js
!studio-app/src/analytics.js
!studio-app/src/analytics.test.js
Expand Down
10 changes: 7 additions & 3 deletions studio-app/src/StoryboardComponents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import {
queuePositions,
} from './storyboard-ui-model.js';
import { useStoryboardUI as useStore } from './storyboard-ui-context.jsx';
import { useShotReadiness } from './shot-readiness.js';
import { StoryboardNameDialog } from './StoryboardDialog.jsx';
import { createStoryboardPlayback } from './storyboard-playback.js';

Expand Down Expand Up @@ -1578,6 +1579,8 @@ export function ShotCard({
const [picker, setPicker] = useState(null);
const { setNodeRef, attributes, listeners, style, isOver, active } = useShotSortable(shot.id);
const info = expand(shot, db, locale);
const missing = useShotReadiness(shot, tr);
const warnings = [...info.warnings, ...missing];
const status = shotStatus(shot, queuePositions(state.shots)[shot.id], locale);
const latest = shot.versions[shot.versions.length - 1];
const usual = usualSettings(state.shots, db, locale);
Expand Down Expand Up @@ -1648,8 +1651,8 @@ export function ShotCard({
{shot.quality !== usual.quality && (
<span className="q">{QUALITY_LABEL[shot.quality]}</span>
)}
{info.warnings.length > 0 && (
<span className="warnbadge" title={info.warnings.join('\n')}>
{warnings.length > 0 && (
<span className="warnbadge" title={warnings.join('\n')}>
!
</span>
)}
Expand Down Expand Up @@ -1818,6 +1821,7 @@ export function ShotEditor({ shotId, onClose, inline, wide, onWide }) {
const { MODE_LABEL, QUALITY_LABEL } = displayLabels(tr);
const { state, dispatch, db } = useStore();
const shot = state.shots.find((s) => s.id === shotId);
const missing = useShotReadiness(shot, tr);
const [copies, setCopies] = useState('1');
// Hooks stay above the early return below (rules of hooks).
const [regenerating, setRegenerating] = useState(false);
Expand Down Expand Up @@ -2469,7 +2473,7 @@ export function ShotEditor({ shotId, onClose, inline, wide, onWide }) {
<WorkbenchCopies value={copies} onChange={setCopies} max={state.copiesMax} />
</div>
{/* Advice right above Generate, always visible (h3-studio#739). */}
{info.hints?.map((hint) => (
{[...(info.hints || []), ...missing].map((hint) => (
<p key={hint} className="ed-hint">
{hint}
</p>
Expand Down
1 change: 1 addition & 0 deletions studio-app/src/StoryboardConnected.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
8 changes: 8 additions & 0 deletions studio-app/src/cloud-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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),
Expand Down
8 changes: 8 additions & 0 deletions studio-app/src/i18n/legacyCatalogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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':
Expand Down
22 changes: 22 additions & 0 deletions studio-app/src/shot-readiness.js
Original file line number Diff line number Diff line change
@@ -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);
}
11 changes: 11 additions & 0 deletions studio-app/src/storyboard-domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 : '—');
Expand Down
19 changes: 19 additions & 0 deletions studio-app/src/storyboard-domain.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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), []);
});
Loading