cozyclay 3.1.2 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +1 -1
  3. package/bin/agent/agent-runner.mjs +160 -151
  4. package/bin/agent/agent-stream.mjs +86 -0
  5. package/bin/agent/anthropic-subscription-events.mjs +120 -0
  6. package/bin/agent/anthropic-subscription-prompt.mjs +116 -0
  7. package/bin/agent/anthropic-subscription-stream.mjs +251 -0
  8. package/bin/agent/anthropic-subscription.mjs +73 -0
  9. package/bin/agent/claude-code-executable.mjs +93 -0
  10. package/bin/agent/cliproxy-provider.mjs +25 -11
  11. package/bin/agent/model-roles.mjs +112 -0
  12. package/bin/agent/openai-compatible-provider.mjs +95 -0
  13. package/bin/agent/pi-tools.mjs +19 -10
  14. package/bin/agent/provider-login.mjs +3 -1
  15. package/bin/agent/provider-routes.mjs +2 -0
  16. package/bin/agent/providers.mjs +35 -130
  17. package/bin/cozyclay.mjs +2 -48
  18. package/bin/star-prompt.mjs +123 -0
  19. package/dist/app/index.html +5 -5
  20. package/dist/assets/TaskBoard-U1Yl-4F1.js +1 -0
  21. package/dist/assets/app-BdRlMf4J.css +1 -0
  22. package/dist/assets/app-KXdgBC-S.js +5144 -0
  23. package/dist/assets/{motion3-compile-dgGhrvPK.js → motion3-compile-DDG3wklD.js} +1 -1
  24. package/dist/assets/scene-asset-sync-Bz59Ghdz.js +13 -0
  25. package/dist/assets/task-board-DFfNa0xg.js +15 -0
  26. package/dist/assets/{workflow-C87p6E1E.js → workflow-BMJGd_gZ.js} +1 -1
  27. package/dist/cozyclay-package.json +1 -1
  28. package/dist/workflow/index.html +3 -3
  29. package/package.json +6 -4
  30. package/src/App.jsx +28 -28
  31. package/src/agent-image-client.js +2 -2
  32. package/src/asset-pane.jsx +14 -14
  33. package/src/character-kind.js +13 -12
  34. package/src/commands/project.js +2 -1
  35. package/src/commands/shot.js +1 -1
  36. package/src/commands/view.js +2 -2
  37. package/src/domains/cast.js +15 -15
  38. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  39. package/src/domains/motion.js +11 -11
  40. package/src/domains/objects.js +20 -30
  41. package/src/domains/scenes.js +14 -21
  42. package/src/domains/shots.js +3 -3
  43. package/src/first-success-guide.jsx +19 -3
  44. package/src/hierarchy-panel.jsx +6 -7
  45. package/src/locale-zh/app.js +124 -0
  46. package/src/locale-zh/domains.js +36 -0
  47. package/src/locale-zh/labels.js +148 -0
  48. package/src/locale-zh/panels.js +225 -0
  49. package/src/locale-zh/shell.js +230 -0
  50. package/src/locale-zh/timeline.js +145 -0
  51. package/src/locale-zh/workflow.js +104 -0
  52. package/src/locale-zh/workspace.js +190 -0
  53. package/src/locale-zh.js +21 -0
  54. package/src/locale.js +16 -6
  55. package/src/main.jsx +1 -1
  56. package/src/motion/beat-board-model.js +4 -4
  57. package/src/motion/beat-board.jsx +1 -1
  58. package/src/motion/ik-pick.js +24 -0
  59. package/src/motion/take-segments.jsx +5 -5
  60. package/src/motion/timeline.jsx +30 -40
  61. package/src/motion/waypoints.js +6 -6
  62. package/src/object-catalog.jsx +4 -5
  63. package/src/panels/CameraPanel.jsx +3 -3
  64. package/src/panels/HdrEnvironmentControls.jsx +2 -1
  65. package/src/panels/ObjectTransformPanel.jsx +19 -29
  66. package/src/panels/PoseImagePanel.jsx +8 -8
  67. package/src/panels/PosePanel.jsx +2 -2
  68. package/src/panels/ReferenceImageField.jsx +2 -2
  69. package/src/panels/RigControlPanel.jsx +2 -2
  70. package/src/panels/SculptPanel.jsx +9 -9
  71. package/src/panels/SubjectBox.jsx +3 -3
  72. package/src/panels/SubjectsPanel.jsx +1 -1
  73. package/src/posestudio.jsx +10 -8
  74. package/src/project-browser.css +7 -6
  75. package/src/project-browser.jsx +7 -6
  76. package/src/project-open-file.js +19 -0
  77. package/src/qa-label.js +1 -1
  78. package/src/range-pin-panel.jsx +14 -14
  79. package/src/resource-status.jsx +3 -3
  80. package/src/result-modal.jsx +10 -10
  81. package/src/settings-menu.jsx +4 -1
  82. package/src/shell/BoardSlot.jsx +5 -5
  83. package/src/shell/CompositionGuideButton.jsx +1 -1
  84. package/src/shell/GhostsMenuGroup.jsx +1 -1
  85. package/src/shell/StatusBar.jsx +4 -4
  86. package/src/shell/ViewportToolbar.jsx +3 -3
  87. package/src/shell/add-camera-shot.js +2 -2
  88. package/src/shell/details-selection.js +1 -1
  89. package/src/shell/preference-controls.jsx +2 -1
  90. package/src/shot-guides.js +5 -5
  91. package/src/stage-hdr.js +3 -3
  92. package/src/stage-labels-ko.js +2 -2
  93. package/src/stage-startup-defaults.js +3 -3
  94. package/src/studio-actions.js +2 -2
  95. package/src/styles/tokens.css +1 -1
  96. package/src/styles.css +1 -1
  97. package/src/use-case-question.jsx +18 -18
  98. package/src/workflow/AgentPanel.jsx +4 -4
  99. package/src/workflow/AgentPlanCard.jsx +1 -1
  100. package/src/workflow/AgentProgressCard.jsx +4 -4
  101. package/src/workflow/AgentTurnSteps.jsx +1 -1
  102. package/src/workflow/LoginPanel.jsx +1 -1
  103. package/src/workflow/TaskBoard.jsx +1 -1
  104. package/src/workflow/TaskInline.jsx +1 -1
  105. package/src/workflow/agent-client.js +1 -1
  106. package/src/workflow/agent-turn.js +5 -5
  107. package/src/workflow/task-board.js +4 -4
  108. package/tools/loc-baseline.json +2 -2
  109. package/tools/run-tests.mjs +5 -5
  110. package/dist/assets/TaskBoard-GR8xZm5c.js +0 -1
  111. package/dist/assets/app-Bhe0TpGz.css +0 -1
  112. package/dist/assets/app-C4QTcDcb.js +0 -5144
  113. package/dist/assets/scene-asset-sync-qoxcdrhj.js +0 -13
  114. package/dist/assets/task-board-BqReHzOi.js +0 -15
@@ -31,7 +31,7 @@ import * as THREE from "three";
31
31
  import { StudioProtocolError } from "../studio-agent-protocol.js";
32
32
  import { applyTakeFrame } from "../motion3/playback.js";
33
33
  import { advancePlayback, installPlaybackApplier } from "./playback.js";
34
- import { isKo, ko } from "../locale.js";
34
+ import { ko } from "../locale.js";
35
35
  import { isProxyFigure } from "../scenes.js";
36
36
  import { refuseRigOnly } from "../character-kind.js";
37
37
  import { DEFAULT_POSE, restoreBindPositions, applyPose, applyHipsOffset } from "../poses.js";
@@ -439,9 +439,9 @@ export function useMotion(appContext) {
439
439
  ikPreDragRef.current = null;
440
440
  if (pending.rig) appContext.shared.poseMemberAtFrame(pending.rig, motion, pending.state, appContext.shared.tlFrame, IK_CORRECTION_BLEND_FRAMES);
441
441
  setIkTick((value) => value + 1);
442
- appContext.notify(isKo
443
- ? `IK 보정을 ${pending.startFrame}–${pending.endFrame}프레임에 적용했어요`
444
- : `Applied IK correction across frames ${pending.startFrame}–${pending.endFrame}`);
442
+ appContext.notify(ko(`Applied IK correction across frames ${pending.startFrame}–${pending.endFrame}`,
443
+ `IK 보정을 ${pending.startFrame}–${pending.endFrame}프레임에 적용했어요`,
444
+ `已在第 ${pending.startFrame}–${pending.endFrame} 帧应用 IK 修正`));
445
445
  return true;
446
446
  }
447
447
 
@@ -496,8 +496,8 @@ export function useMotion(appContext) {
496
496
  setRangePinSelection(resolved.pin.id);
497
497
  setRangePinPreview({ draft: { ...draft, id: resolved.pin.id }, track: resolved.pin.track, target: resolved.targetWorld.toArray(), conflictFrames: [], overlapError: "" });
498
498
  appContext.shared.setTlPlaying(false);
499
- appContext.notify(isKo ? `${resolved.pin.track} 고정을 적용했어요 · ${resolved.pin.startFrame}–${resolved.pin.endFrame}프레임` : `Applied ${resolved.pin.track} pin · frames ${resolved.pin.startFrame}–${resolved.pin.endFrame}`);
500
- } catch (error) { appContext.notify(ko(`Could not apply pin — ${error.message}`, `고정을 적용하지 못했어요 — ${error.message}`)); }
499
+ appContext.notify(ko(`Applied ${resolved.pin.track} pin · frames ${resolved.pin.startFrame}–${resolved.pin.endFrame}`, `${resolved.pin.track} 고정을 적용했어요 · ${resolved.pin.startFrame}–${resolved.pin.endFrame}프레임`, `已应用 ${resolved.pin.track} 固定 · 第 ${resolved.pin.startFrame}–${resolved.pin.endFrame} 帧`));
500
+ } catch (error) { appContext.notify(ko(`Could not apply pin — ${error.message}`, `고정을 적용하지 못했어요 — ${error.message}`, `无法应用固定 — ${error.message}`)); }
501
501
  }
502
502
  function deleteRangePin(pinId) {
503
503
  const state = ensureRangePinState(appContext.shared.ikStateRef.current), pin = state.pins.get(pinId);
@@ -512,7 +512,7 @@ export function useMotion(appContext) {
512
512
  liveState.pinResiduals.delete(pinId);
513
513
  liveState.tracked = new Set([...liveState.keys.values()].flatMap((entry) => [...entry.keys()]));
514
514
  setRangePinSelection(null); setRangePinPreview(null); setIkTick((value) => value + 1);
515
- appContext.notify(isKo ? `${pin.track} 고정을 삭제했어요` : `Deleted ${pin.track} pin`);
515
+ appContext.notify(ko(`Deleted ${pin.track} pin`, `${pin.track} 고정을 삭제했어요`, `已删除 ${pin.track} 固定`));
516
516
  }
517
517
 
518
518
  /** Deep copy of an IK state's key map: frame → Map(trackId → {q,p}), with
@@ -881,7 +881,7 @@ export function useMotion(appContext) {
881
881
  function ikAddKeyframe() {
882
882
  if (!ikChains) return;
883
883
  if (!keyIkPoseAtPlayhead()) return;
884
- appContext.notify(isKo ? `${appContext.shared.tlFrame}프레임에 전신 IK 키를 추가했어요` : `Full-body IK key at frame ${appContext.shared.tlFrame}`);
884
+ appContext.notify(ko(`Full-body IK key at frame ${appContext.shared.tlFrame}`, `${appContext.shared.tlFrame}프레임에 전신 IK 키를 추가했어요`, `已在第 ${appContext.shared.tlFrame} 帧添加全身 IK 关键帧`));
885
885
  }
886
886
 
887
887
  function ikDeleteKeyframe(frame) {
@@ -918,9 +918,9 @@ export function useMotion(appContext) {
918
918
  // Handles re-seat on the posed effectors, ready to drag into a refinement.
919
919
  ikSeedTargets(ikChains, appContext.shared.ikStateRef.current);
920
920
  setIkTick((n) => n + 1);
921
- appContext.notify(isKo
922
- ? `${appContext.shared.tlFrame}프레임에 포즈를 전신 IK 보정 키로 추가했어요 — 모션은 그대로예요`
923
- : `Pose keyed as a full-body IK correction at frame ${appContext.shared.tlFrame} — the take stays`);
921
+ appContext.notify(ko(`Pose keyed as a full-body IK correction at frame ${appContext.shared.tlFrame} — the take stays`,
922
+ `${appContext.shared.tlFrame}프레임에 포즈를 전신 IK 보정 키로 추가했어요 — 모션은 그대로예요`,
923
+ `已在第 ${appContext.shared.tlFrame} 帧将姿势记录为全身 IK 修正关键帧 — 动作保持不变`));
924
924
  return true;
925
925
  }
926
926
 
@@ -31,7 +31,7 @@ import {
31
31
  tidyAngle,
32
32
  } from "../scene-objects.js";
33
33
 
34
- import { ko, isKo } from "../locale.js";
34
+ import { ko } from "../locale.js";
35
35
  import {
36
36
  sceneObjectNameDisplayKo,
37
37
  attachWorldMatrix,
@@ -264,7 +264,7 @@ export function useObjects(appContext) {
264
264
  return;
265
265
  }
266
266
  changeSceneObject(object.id, patch);
267
- appContext.notify(isKo ? `${sceneObjectNameDisplayKo(object.name)}을 표면 위에 내려놓았어요` : `${object.name} dropped to surface`);
267
+ appContext.notify(ko(`${sceneObjectNameDisplayKo(object.name)} dropped to surface`, `${sceneObjectNameDisplayKo(object.name)}을 표면 위에 내려놓았어요`, `${sceneObjectNameDisplayKo(object.name)}已放到表面`));
268
268
  }
269
269
 
270
270
  // How much of the wall counts as the wall, and how wide the brush that
@@ -314,7 +314,7 @@ export function useObjects(appContext) {
314
314
  // swallows the very next W/E/R. Renaming stays on F2/Return and the row's
315
315
  // context menu. (docs/unity-reference.md §9.7)
316
316
  setGizmoMode("move");
317
- appContext.notify(isKo ? `${sceneObjectNameDisplayKo(object.name)} 추가됨 — W 이동, E 회전, R 크기` : `${object.name} added — W move, E rotate, R scale`);
317
+ appContext.notify(ko(`${sceneObjectNameDisplayKo(object.name)} added — W move, E rotate, R scale`, `${sceneObjectNameDisplayKo(object.name)} 추가됨 — W 이동, E 회전, R 크기`, `${sceneObjectNameDisplayKo(object.name)}已添加 — W 移动,E 旋转,R 缩放`));
318
318
  }
319
319
 
320
320
  /** "Sofa 2.png" reads as a set piece; "sofa-2.png" does not. The extension
@@ -345,11 +345,9 @@ export function useObjects(appContext) {
345
345
  placement,
346
346
  );
347
347
  const imported = publishImported(object, commandContext);
348
- appContext.notify(
349
- isKo
350
- ? `${object.name} 추가됨 — 실제 높이(m)를 입력하면 크기가 맞습니다`
351
- : `${object.name} added — type its real height in metres to set the scale`,
352
- );
348
+ appContext.notify(ko(`${object.name} added — type its real height in metres to set the scale`,
349
+ `${object.name} 추가됨 — 실제 높이(m)를 입력하면 크기가 맞습니다`,
350
+ `${object.name} 已添加 — 输入实际高度(米)即可匹配尺寸`));
353
351
  return imported;
354
352
  } catch (error) {
355
353
  throw new StudioProtocolError(error.code ?? "INVALID_ARGUMENT", `Could not import that image: ${error.message}`);
@@ -379,11 +377,9 @@ export function useObjects(appContext) {
379
377
  placement,
380
378
  );
381
379
  const imported = publishImported(object, commandContext);
382
- appContext.notify(
383
- isKo
384
- ? `${object.name} 추가됨 — 실제 높이(m)를 입력하면 크기가 맞습니다`
385
- : `${object.name} added — type its real height in metres to set the scale`,
386
- );
380
+ appContext.notify(ko(`${object.name} added — type its real height in metres to set the scale`,
381
+ `${object.name} 추가됨 — 실제 높이(m)를 입력하면 크기가 맞습니다`,
382
+ `${object.name} 已添加 — 输入实际高度(米)即可匹配尺寸`));
387
383
  return imported;
388
384
  }
389
385
 
@@ -479,11 +475,9 @@ export function useObjects(appContext) {
479
475
  placementInFrontOfShot(),
480
476
  );
481
477
  const imported = publishImported(object, commandContext);
482
- appContext.notify(
483
- isKo
484
- ? `${object.name} 추가됨 — 실제 높이(m)를 입력하면 크기가 맞습니다`
485
- : `${object.name} added — type its real height in metres to set the scale`,
486
- );
478
+ appContext.notify(ko(`${object.name} added — type its real height in metres to set the scale`,
479
+ `${object.name} 추가됨 — 실제 높이(m)를 입력하면 크기가 맞습니다`,
480
+ `${object.name} 已添加 — 输入实际高度(米)即可匹配尺寸`));
487
481
  return imported;
488
482
  } catch (error) {
489
483
  throw new StudioProtocolError(error.code ?? "INVALID_ARGUMENT", `Could not import that model: ${error.message}`);
@@ -520,11 +514,9 @@ export function useObjects(appContext) {
520
514
  placement,
521
515
  );
522
516
  const imported = publishImported(object, commandContext);
523
- appContext.notify(
524
- isKo
525
- ? `${object.name} 추가됨 — 실제 높이(m)를 입력하면 크기가 맞습니다`
526
- : `${object.name} added — type its real height in metres to set the scale`,
527
- );
517
+ appContext.notify(ko(`${object.name} added — type its real height in metres to set the scale`,
518
+ `${object.name} 추가됨 — 실제 높이(m)를 입력하면 크기가 맞습니다`,
519
+ `${object.name} 已添加 — 输入实际高度(米)即可匹配尺寸`));
528
520
  return imported;
529
521
  }
530
522
 
@@ -569,11 +561,9 @@ export function useObjects(appContext) {
569
561
  aspect: cut.asset.width / cut.asset.height,
570
562
  height: fullFrameHeight * cut.heightScale,
571
563
  })));
572
- appContext.notify(
573
- isKo
574
- ? `${object.name} 배경 제거 — ${Math.round(cut.removed * 100)}% 지움. 원본과 칠한 영역은 그대로 남습니다`
575
- : `${object.name} — ${Math.round(cut.removed * 100)}% removed. The original and your selection are kept`,
576
- );
564
+ appContext.notify(ko(`${object.name} — ${Math.round(cut.removed * 100)}% removed. The original and your selection are kept`,
565
+ `${object.name} 배경 제거 — ${Math.round(cut.removed * 100)}% 지움. 원본과 칠한 영역은 그대로 남습니다`,
566
+ `${object.name} 背景已移除 — 清除 ${Math.round(cut.removed * 100)}%。原图和选区仍保留`));
577
567
  return { objectId: object.id };
578
568
  } catch (error) {
579
569
  throw new StudioProtocolError(error.code ?? "TARGET_NOT_READY", `Could not remove the background: ${error.message}`);
@@ -606,7 +596,7 @@ export function useObjects(appContext) {
606
596
  const placed = { ...object, id: copy.id, name: copy.name, x: object.x + 0.5 };
607
597
  domain.write((objects) => [...objects, placed]);
608
598
  appContext.shared.setSelectedHierarchyId(`object:${placed.id}`);
609
- appContext.notify((isKo, ko) => isKo ? `${sceneObjectNameDisplayKo(placed.name)} 복제됨` : `${placed.name} duplicated`);
599
+ appContext.notify((isKo, ko) => ko(`${placed.name} duplicated`, `${sceneObjectNameDisplayKo(placed.name)} 복제됨`, `${sceneObjectNameDisplayKo(placed.name)}已复制`));
610
600
  }
611
601
 
612
602
  function frameSelection(id = selectedSceneObjectId) {
@@ -676,7 +666,7 @@ export function useObjects(appContext) {
676
666
  ? ko("Character 1", "인물 1")
677
667
  : index === 1
678
668
  ? ko("Character 2", "인물 2")
679
- : isKo ? `인물 ${index + 1}` : `Character ${index + 1}`;
669
+ : ko(`Character ${index + 1}`, `인물 ${index + 1}`, `角色 ${index + 1}`);
680
670
  const bone = attach.bone
681
671
  ? HIERARCHY_INSPECTOR_TITLES[`rig.${attach.bone}`] ?? attach.bone
682
672
  : ko("Root", "루트");
@@ -30,9 +30,7 @@ import {
30
30
  PROJECT_EXTENSION,
31
31
  normalizeWorkflowGraph,
32
32
  storeWorkflowGraph,
33
- pickProjectFileForOpen,
34
33
  readProjectFile,
35
- openProjectFallback,
36
34
  readProjectDocument,
37
35
  requestHandlePermission,
38
36
  createWorkflowGraph,
@@ -42,11 +40,12 @@ import {
42
40
  buildAnimationProjectFromStoryboard,
43
41
  } from "../project.js";
44
42
  import { playgroundSceneUrl, fetchSceneProject } from "../playground.js";
43
+ import { pickProjectToOpen } from "../project-open-file.js";
45
44
  import { openAssetDb, referencedAssetIds, getAsset, putAsset } from "../scene-assets.js";
46
45
  import { internWorkflowOutputs, workflowOutputRefs, resolveWorkflowOutputs } from "../workflow/workflow-resources.js";
47
46
 
48
47
  import { resourceManifest } from "../project-resources.js";
49
- import { isKo, ko } from "../locale.js";
48
+ import { ko } from "../locale.js";
50
49
  import { track, bucketCount, bucketProjectAge, startExportAttempt } from "../analytics.js";
51
50
  import { mergeProjectCustomPoses } from "../project-poses.js";
52
51
  import { DEFAULT_WORKSPACE_LAYOUT, DEFAULT_DURATION_S, TIMELINE_FPS } from "../app-stage.jsx";
@@ -270,9 +269,9 @@ export function useScenes(appContext) {
270
269
  function projectProblemsNotice(problems) {
271
270
  if (!Array.isArray(problems) || !problems.length) return "";
272
271
  const codes = [...new Set(problems.map((problem) => problem?.code).filter(Boolean))].join(", ");
273
- return isKo
274
- ? ` · 포함된 자원 ${problems.length}개를 건너뛰었어요${codes ? ` (${codes})` : ""}`
275
- : ` · skipped ${problems.length} embedded resource${problems.length === 1 ? "" : "s"}${codes ? ` (${codes})` : ""}`;
272
+ return ko(` · skipped ${problems.length} embedded resource${problems.length === 1 ? "" : "s"}${codes ? ` (${codes})` : ""}`,
273
+ ` · 포함된 자원 ${problems.length}개를 건너뛰었어요${codes ? ` (${codes})` : ""}`,
274
+ ` · 已跳过 ${problems.length} 个嵌入资源${codes ? `(${codes})` : ""}`);
276
275
  }
277
276
 
278
277
  async function rehydrateProjectAssets(project, warnings = []) {
@@ -344,7 +343,7 @@ export function useScenes(appContext) {
344
343
  object_count_bucket: bucketCount(appContext.shared.projectStateRef.current.sceneObjects?.length ?? 0),
345
344
  shot_count_bucket: bucketCount(appContext.shared.shots.length),
346
345
  });
347
- appContext.notify((isKo, ko) => isKo ? `프로젝트 저장됨: ${name}${PROJECT_EXTENSION}` : `Project saved: ${name}${PROJECT_EXTENSION}`);
346
+ appContext.notify((isKo, ko) => ko(`Project saved: ${name}${PROJECT_EXTENSION}`, `프로젝트 저장됨: ${name}${PROJECT_EXTENSION}`, `项目已保存:${name}${PROJECT_EXTENSION}`));
348
347
  return { saved: true, name, fileName: downloaded ? `${name}${PROJECT_EXTENSION}` : appContext.shared.projectHandleRef.current?.name ?? `${name}${PROJECT_EXTENSION}`, downloaded };
349
348
  } catch (err) {
350
349
  if (err?.name === "AbortError") {
@@ -444,18 +443,12 @@ export function useScenes(appContext) {
444
443
  async function openProject(context = null) {
445
444
  if (!context) return runProject("project.open");
446
445
  try {
447
- let file = null;
448
- let handle = null;
449
- if (hasFileSystemAccess()) {
450
- handle = await pickProjectFileForOpen();
451
- file = await readProjectFile(handle);
452
- } else {
453
- file = await openProjectFallback();
454
- }
446
+ const { file, handle = null, blocked } = await pickProjectToOpen();
447
+ if (blocked) throw new StudioProtocolError("TARGET_NOT_READY", ko("This browser would not read that file. Open it once more.", "이 브라우저가 그 파일을 읽지 못했어요. 한 번 더 열어 주세요."));
455
448
  if (!file) return false;
456
449
  const result = readProjectDocument(file.text);
457
450
  if (!result.ok) {
458
- appContext.notify(isKo ? `프로젝트를 열 수 없어요: ${result.reason}` : `Cannot open project: ${result.reason}`);
451
+ appContext.notify(ko(`Cannot open project: ${result.reason}`, `프로젝트를 열 수 없어요: ${result.reason}`, `无法打开项目:${result.reason}`));
459
452
  return false;
460
453
  }
461
454
  result.project.savedAt = result.project.savedAt ?? file.savedAt ?? null;
@@ -465,7 +458,7 @@ export function useScenes(appContext) {
465
458
  context.check();
466
459
  applyProject(result.project, true);
467
460
  setProjectStartupOpen(false);
468
- appContext.notify(`${isKo ? `프로젝트 열림: ${result.project.name}` : `Project opened: ${result.project.name}`}${projectProblemsNotice(result.problems)}`);
461
+ appContext.notify(`${ko(`Project opened: ${result.project.name}`, `프로젝트 열림: ${result.project.name}`, `项目已打开:${result.project.name}`)}${projectProblemsNotice(result.problems)}`);
469
462
  return true;
470
463
  } catch (err) {
471
464
  if (err?.name === "AbortError") return false;
@@ -490,7 +483,7 @@ export function useScenes(appContext) {
490
483
  const file = await readProjectFile(handle);
491
484
  const result = readProjectDocument(file.text);
492
485
  if (!result.ok) {
493
- appContext.notify(isKo ? `프로젝트를 열 수 없어요: ${result.reason}` : `Cannot open project: ${result.reason}`);
486
+ appContext.notify(ko(`Cannot open project: ${result.reason}`, `프로젝트를 열 수 없어요: ${result.reason}`, `无法打开项目:${result.reason}`));
494
487
  return false;
495
488
  }
496
489
  result.project.savedAt = result.project.savedAt ?? file.savedAt ?? null;
@@ -501,7 +494,7 @@ export function useScenes(appContext) {
501
494
  applyProject(result.project, true);
502
495
  setProjectBrowserOpen(false);
503
496
  setProjectStartupOpen(false);
504
- appContext.notify(`${isKo ? `프로젝트 열림: ${result.project.name}` : `Project opened: ${result.project.name}`}${projectProblemsNotice(result.problems)}`);
497
+ appContext.notify(`${ko(`Project opened: ${result.project.name}`, `프로젝트 열림: ${result.project.name}`, `项目已打开:${result.project.name}`)}${projectProblemsNotice(result.problems)}`);
505
498
  return true;
506
499
  } catch (err) {
507
500
  if (err?.code) throw err;
@@ -543,7 +536,7 @@ export function useScenes(appContext) {
543
536
  setProjectStartupOpen(false);
544
537
  appContext.shared.setFirstSuccessGuideOpen(true);
545
538
  domain.pendingCheckpoint = { clock: appContext.undoClock, name };
546
- appContext.notify(ko(`New project: ${name}`, `새 프로젝트: ${name}`));
539
+ appContext.notify(ko(`New project: ${name}`, `새 프로젝트: ${name}`, `新项目:${name}`));
547
540
  return true;
548
541
  }
549
542
 
@@ -560,7 +553,7 @@ export function useScenes(appContext) {
560
553
  await rehydrateProjectAssets(result.project, result.warnings);
561
554
  context.check();
562
555
  applyProject(result.project, true);
563
- appContext.notify(`${isKo ? `프로젝트 복원됨: ${result.project.name}` : `Project restored: ${result.project.name}`}${projectProblemsNotice(result.problems)}`);
556
+ appContext.notify(`${ko(`Project restored: ${result.project.name}`, `프로젝트 복원됨: ${result.project.name}`, `项目已恢复:${result.project.name}`)}${projectProblemsNotice(result.problems)}`);
564
557
  return true;
565
558
  } catch (error) {
566
559
  if (error?.code) throw error;
@@ -16,7 +16,7 @@ import { followFramingFromCamera, craneHeightAt, buildRail, simplifyStroke } fro
16
16
  import { defaultRailRange, clampRailRange } from '../camera-rail-schedule.js';
17
17
  import { timelineSpan, timelineContentExtent } from '../timeline-extent.js';
18
18
  import { trackFeature } from '../analytics.js';
19
- import { ko, isKo } from '../locale.js';
19
+ import { ko } from '../locale.js';
20
20
 
21
21
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
22
22
  function normalizeState(value) {
@@ -367,9 +367,9 @@ export function useShots(appContext) {
367
367
  // In the landing playground a drawn rail opens the player
368
368
  // on the shot camera, but leave playback to the visitor.
369
369
  appContext.shared.enterPreview(); appContext.shared.setTlPlaying(false); setTlFrame(activeShot.startFrame);
370
- appContext.notify(isKo ? '레일 완성 — 샷 카메라 시점으로 전환했습니다. ▶ 로 재생, Esc 로 복귀' : 'Rail drawn — you are looking through the shot camera. Press ▶ to ride it; Esc goes back to flying.'); return;
370
+ appContext.notify(ko('Rail drawn — you are looking through the shot camera. Press ▶ to ride it; Esc goes back to flying.', '레일 완성 — 샷 카메라 시점으로 전환했습니다. ▶ 로 재생, Esc 로 복귀', '轨道已完成 — 已切换到镜头摄影机视角。按 ▶ 播放,按 Esc 返回自由视角。')); return;
371
371
  }
372
- appContext.notify(isKo ? `카메라 레일 완성 — ${curve ? curve.length.toFixed(1) : '?'} m, 제어점 ${simplified.length}개` : `Camera rail drawn — ${curve ? curve.length.toFixed(1) : '?'} m, ${simplified.length} control points`);
372
+ appContext.notify(ko(`Camera rail drawn — ${curve ? curve.length.toFixed(1) : '?'} m, ${simplified.length} control points`, `카메라 레일 완성 — ${curve ? curve.length.toFixed(1) : '?'} m, 제어점 ${simplified.length}개`, `摄影机轨道已完成 — ${curve ? curve.length.toFixed(1) : '?'} m,${simplified.length} 个控制点`));
373
373
  }
374
374
  function publishStudioShots(value) { return appContext.storeDomain('shot').publish(value); }
375
375
  function commitStudioShots(draft) { return appContext.storeDomain('shot').commitDraft(draft); }
@@ -5,26 +5,34 @@ const ANIMATION_STEPS = [
5
5
  {
6
6
  en: "Select a character in the Outliner.",
7
7
  ko: "아웃라이너에서 캐릭터를 선택하세요.",
8
+ zh: "在大纲视图中选择一个角色。",
8
9
  hintEn: "Click Character 1 (or its row) to make it active.",
9
10
  hintKo: "캐릭터 1(또는 캐릭터 행)을 클릭하면 활성화됩니다.",
11
+ hintZh: "点击角色 1(或其所在行)使其成为当前角色。",
10
12
  },
11
13
  {
12
14
  en: "Move it in the Scene view.",
13
15
  ko: "장면 보기에서 캐릭터를 움직이세요.",
16
+ zh: "在场景视图中移动它。",
14
17
  hintEn: "Drag the character marker on the floor to place it.",
15
18
  hintKo: "바닥의 캐릭터 마커를 드래그해 배치하세요.",
19
+ hintZh: "拖动地面上的角色标记来摆放位置。",
16
20
  },
17
21
  {
18
22
  en: "Ask the Agent for a motion.",
19
23
  ko: "에이전트에게 동작을 요청하세요.",
24
+ zh: "让智能体生成一个动作。",
20
25
  hintEn: "Open it with Cmd/Ctrl+B and say what the character does; the take lands on the Body lane.",
21
26
  hintKo: "Cmd/Ctrl+B로 열고 캐릭터가 할 동작을 말하면 테이크가 바디 레인에 올라갑니다.",
27
+ hintZh: "按 Cmd/Ctrl+B 打开,说出角色要做的动作;动作片段会落在身体轨道上。",
22
28
  },
23
29
  {
24
30
  en: "Scrub the timeline or press Space to play.",
25
31
  ko: "타임라인을 문지르거나 Space를 눌러 재생하세요.",
32
+ zh: "拖动时间线或按空格播放。",
26
33
  hintEn: "Seeing the playhead move is your first visible result.",
27
34
  hintKo: "재생 헤드가 움직이면 첫 결과를 확인한 것입니다.",
35
+ hintZh: "看到播放头移动,就是你的第一个可见结果。",
28
36
  },
29
37
  ];
30
38
 
@@ -32,26 +40,34 @@ const STORYBOARD_STEPS = [
32
40
  {
33
41
  en: "Open the Agent panel (Cmd/Ctrl+B).",
34
42
  ko: "에이전트 패널을 여세요 (Cmd/Ctrl+B).",
43
+ zh: "打开智能体面板(Cmd/Ctrl+B)。",
35
44
  hintEn: "The Agent panel is where you describe each panel.",
36
45
  hintKo: "에이전트 패널에서 각 패널을 설명합니다.",
46
+ hintZh: "在智能体面板中逐个描述每个面板。",
37
47
  },
38
48
  {
39
49
  en: "Describe one scene in a sentence.",
40
50
  ko: "장면 하나를 한 문장으로 설명하세요.",
51
+ zh: "用一句话描述一个场景。",
41
52
  hintEn: "One sentence per turn is enough for one panel.",
42
53
  hintKo: "패널 하나에는 한 문장이면 충분합니다.",
54
+ hintZh: "每个面板一轮只说一句话就够了。",
43
55
  },
44
56
  {
45
57
  en: "Your panel appears on the Board.",
46
58
  ko: "패널이 보드에 나타납니다.",
59
+ zh: "你的面板会出现在看板上。",
47
60
  hintEn: "Open the Board tab to see the new panel card.",
48
61
  hintKo: "보드 탭에서 새 패널 카드를 확인하세요.",
62
+ hintZh: "打开看板标签页查看新的面板卡片。",
49
63
  },
50
64
  {
51
65
  en: "Stylize it, or export the panel pack.",
52
66
  ko: "스타일을 입히거나 패널 팩으로 내보내세요.",
67
+ zh: "对它进行风格化,或导出面板包。",
53
68
  hintEn: "Use Stylize on the card, or export the panel pack.",
54
69
  hintKo: "카드에서 스타일을 입히거나 패널 팩으로 내보내세요.",
70
+ hintZh: "在卡片上使用风格化,或导出面板包。",
55
71
  },
56
72
  ];
57
73
 
@@ -73,7 +89,7 @@ export default function FirstSuccessGuide({ open, onDismiss, previsMode = "anima
73
89
  <div className="v2-first-success-guide-head first-success-guide-head">
74
90
  <div>
75
91
  <strong id="first-success-guide-title">{ko("Your first 60 seconds", "첫 60초")}</strong>
76
- <span>{complete ? ko("Core loop complete", "핵심 흐름 완료") : ko(`Step ${step + 1} of ${STEPS.length}`, `${step + 1} / ${STEPS.length} 단계`)}</span>
92
+ <span>{complete ? ko("Core loop complete", "핵심 흐름 완료", "核心流程完成") : ko(`Step ${step + 1} of ${STEPS.length}`, `${step + 1} / ${STEPS.length} 단계`, `第 ${step + 1} / ${STEPS.length} 步`)}</span>
77
93
  </div>
78
94
  <button type="button" className="v2-first-success-guide-close first-success-guide-close" onClick={onDismiss} aria-label={ko("Dismiss guide", "가이드 닫기")}>×</button>
79
95
  </div>
@@ -91,12 +107,12 @@ export default function FirstSuccessGuide({ open, onDismiss, previsMode = "anima
91
107
  {STEPS.map((item, index) => (
92
108
  <li key={item.en} className={index < step ? "done" : index === step ? "current" : ""}>
93
109
  <span className="v2-first-success-guide-step-mark first-success-guide-step-mark">{index < step ? "✓" : index + 1}</span>
94
- <div><strong>{ko(item.en, item.ko)}</strong>{index === step && <span>{ko(item.hintEn, item.hintKo)}</span>}</div>
110
+ <div><strong>{ko(item.en, item.ko, item.zh)}</strong>{index === step && <span>{ko(item.hintEn, item.hintKo, item.hintZh)}</span>}</div>
95
111
  </li>
96
112
  ))}
97
113
  </ol>
98
114
  <button type="button" className="v2-first-success-guide-next first-success-guide-next btn primary" onClick={() => setStep((value) => value + 1)}>
99
- {ko(step === STEPS.length - 1 ? "Mark complete" : "I did this", step === STEPS.length - 1 ? "완료로 표시" : "했어요")}
115
+ {ko(step === STEPS.length - 1 ? "Mark complete" : "I did this", step === STEPS.length - 1 ? "완료로 표시" : "했어요", step === STEPS.length - 1 ? "标记完成" : "我做了这一步")}
100
116
  </button>
101
117
  </>
102
118
  )}
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
- import { ko, isKo } from "./locale.js";
3
+ import { ko } from "./locale.js";
4
4
  import { buildHierarchyNodes } from "./hierarchy-model.js";
5
5
  import { sceneObjectIdFromHierarchy } from "./scene-objects.js";
6
6
  import { CatalogueEntries, displayObjectLabel } from "./object-catalog.jsx";
@@ -62,9 +62,8 @@ export const HIERARCHY_DRAG_MIME = "application/x-cclay-hierarchy";
62
62
  const carriesHierarchyRow = (event) => !!event.dataTransfer?.types?.includes?.(HIERARCHY_DRAG_MIME);
63
63
 
64
64
  function displaySceneName(name) {
65
- if (!isKo) return name;
66
65
  const generatedName = /^SCENE\s+(\d+)$/i.exec(name);
67
- return generatedName ? ko(`Scene ${generatedName[1]}`, `장면 ${generatedName[1]}`) : name;
66
+ return generatedName ? ko(name, `장면 ${generatedName[1]}`, `场景 ${generatedName[1]}`) : name;
68
67
  }
69
68
 
70
69
  /**
@@ -112,7 +111,7 @@ function ScenePill({ scenes, activeSceneId, onSceneSelect, onSceneCreate, onRena
112
111
  function displayHierarchyLabel(node) {
113
112
  if (node.kind === "object") return displayObjectLabel(node.label);
114
113
  if (node.kind === "scene") return displaySceneName(node.label);
115
- return isKo ? (HIERARCHY_LABELS_KO[node.label] ?? node.label) : node.label;
114
+ return HIERARCHY_LABELS_KO[node.label] ? ko(node.label, HIERARCHY_LABELS_KO[node.label]) : node.label;
116
115
  }
117
116
 
118
117
  const OUTLINER_TYPES = {
@@ -466,7 +465,7 @@ function TreeRow({
466
465
  <button
467
466
  type="button"
468
467
  className="hierarchy-toggle"
469
- aria-label={isKo ? `${label} ${expanded ? "접기" : "펼치기"}` : `${expanded ? "Collapse" : "Expand"} ${label}`}
468
+ aria-label={ko(`${expanded ? "Collapse" : "Expand"} ${label}`, `${label} ${expanded ? "접기" : "펼치기"}`, `${expanded ? "折叠" : "展开"} ${label}`)}
470
469
  onClick={() => onToggle(node.id)}
471
470
  >
472
471
  {expanded ? "▾" : "▸"}
@@ -486,7 +485,7 @@ function TreeRow({
486
485
  ref={inputRef}
487
486
  className="hierarchy-rename-input"
488
487
  defaultValue={node.label}
489
- aria-label={isKo ? `${label} 이름 바꾸기` : `Rename ${label}`}
488
+ aria-label={ko(`Rename ${label}`, `${label} 이름 바꾸기`, `重命名 ${label}`)}
490
489
  autoFocus
491
490
  onFocus={(event) => event.currentTarget.select()}
492
491
  onKeyDown={(event) => {
@@ -523,7 +522,7 @@ function TreeRow({
523
522
  className="hierarchy-eye"
524
523
  data-hidden={node.hidden ? "true" : undefined}
525
524
  aria-pressed={node.hidden === true}
526
- aria-label={node.hidden ? (isKo ? `${label} 표시` : `Show ${label}`) : (isKo ? `${label} 숨기기` : `Hide ${label}`)}
525
+ aria-label={node.hidden ? ko(`Show ${label}`, `${label} 표시`, `显示 ${label}`) : ko(`Hide ${label}`, `${label} 숨기기`, `隐藏 ${label}`)}
527
526
  onClick={(event) => {
528
527
  event.preventDefault();
529
528
  event.stopPropagation();
@@ -0,0 +1,124 @@
1
+ // Chinese UI copy for App-level toasts, export status, error boundary and first-run guide.
2
+ export const ZH_APP = {
3
+ "Load a character to pose it — Pose mode edits its rig with IK": "加载角色后才能摆姿势 — 姿势模式用 IK 编辑其骨骼",
4
+ "That picture format is not supported — use PNG, JPG, WebP or GIF": "不支持该图片格式 — 请使用 PNG、JPG、WebP 或 GIF",
5
+ "A newer edit comes after this deletion. Use Undo history instead.": "此删除之后还有更新的编辑。请改用撤销历史。",
6
+ "Double-click the line to add a dot": "双击线条添加一个点",
7
+ "That image is no longer in storage": "该图片已不在存储中",
8
+ "This image's usage changed, so it was not deleted. Please review it again.": "该图片的使用情况已变化,因此未删除。请重新检查。",
9
+ "This image's scene references changed, so it was not deleted. Please review it again.": "该图片的场景引用已变化,因此未删除。请重新检查。",
10
+ "That image is used by a scene and was not deleted": "该图片正被场景使用,未删除",
11
+ "That image is now used by a scene and was not deleted": "该图片现在正被场景使用,未删除",
12
+ "The scene changed while deleting, so the image was kept. Please review storage again.": "删除期间场景发生变化,图片已保留。请重新检查存储。",
13
+ "The live editor is disconnected. Reconnect before sending.": "实时编辑器已断开。请重新连接后再发送。",
14
+ "MP4 encoding is unavailable. Use a current browser with H.264 WebCodecs support (such as Chrome or Edge), enable hardware acceleration, then retry.": "MP4 编码不可用。请使用支持 H.264 WebCodecs 的最新浏览器(如 Chrome 或 Edge),开启硬件加速后重试。",
15
+ "Video encoding failed. Retry the same request. If resources are tight, shorten the shot range or lower output resolution before starting a new export.": "视频编码失败。请重试相同请求。若资源紧张,请缩短镜头范围或降低输出分辨率后重新导出。",
16
+ "Frame rendering failed. Let the scene finish loading, then retry. If it repeats, shorten the range or reduce output resolution for a new export.": "帧渲染失败。请等场景加载完成后再试。若反复出现,请缩短范围或降低输出分辨率后重新导出。",
17
+ "Export cancelled. No further files will be handed off.": "导出已取消。不会再交付其他文件。",
18
+ "Export could not finish. Retry the same request. For memory or resource problems, close other heavy tabs or use a shorter range / lower output resolution in a new export.": "导出未能完成。请重试相同请求。若为内存或资源问题,请关闭其他占用较大的标签页,或在新的导出中使用更短的范围/更低的分辨率。",
19
+ "An export is already running": "已有导出正在进行",
20
+ "Export completed. Check your export folder or browser downloads; the OS save is not confirmed.": "导出已完成。请查看导出文件夹或浏览器下载;系统保存未确认。",
21
+ "Export pipeline completed. The requesting tool handles the file handoff.": "导出流程已完成。文件交付由发起请求的工具处理。",
22
+ "Cancelling at the current work boundary…": "正在当前工作边界处取消…",
23
+ "Preparing": "准备中",
24
+ "Encoding": "编码中",
25
+ "Finalizing": "收尾中",
26
+ "Completed": "已完成",
27
+ "Failed": "失败",
28
+ "Cancelled": "已取消",
29
+ "Depth range sampled": "已采样深度范围",
30
+ "Frames submitted to encoder": "已提交给编码器的帧",
31
+ "Finalizing MP4. Progress is indeterminate; this stage cannot be interrupted.": "正在完成 MP4。进度不确定;此阶段无法中断。",
32
+ "Finishing encoder output. Progress is indeterminate.": "正在完成编码器输出。进度不确定。",
33
+ "Handing off existing PNGs. The file handoff cannot be cancelled.": "正在交付现有 PNG。文件交付无法取消。",
34
+ "Building the ZIP. Cancel takes effect after the current work unit, before the file handoff.": "正在构建 ZIP。取消将在当前工作单元结束后、文件交付前生效。",
35
+ "Cancel stops at the next frame boundary, before the file handoff.": "取消将在下一个帧边界停止,届时尚未交付文件。",
36
+ "Preparing the renderer and checking MP4 support…": "正在准备渲染器并检查 MP4 支持…",
37
+ "Encoding the requested frames, not yet a completed file.": "正在编码所请求的帧,尚未生成完整文件。",
38
+ "Retry keeps the original shot, camera, range and settings; it does not change your edits.": "重试会沿用原镜头、摄影机、范围与设置,不会改变你的编辑。",
39
+ "Cancel export": "取消导出",
40
+ "Retry same export": "重试相同导出",
41
+ "Add at least one Shot before exporting OTIO": "导出 OTIO 前请至少添加一个镜头",
42
+ "The shot renderer is not ready": "镜头渲染器尚未就绪",
43
+ "Add at least one Shot before exporting a keyframe pack": "导出关键帧包前请至少添加一个镜头",
44
+ "Depth and normal passes exported": "深度和法线通道已导出",
45
+ "Add at least one Shot before exporting a storyboard": "导出故事板前请至少添加一个镜头",
46
+ "Composing the storyboard…": "正在编排故事板…",
47
+ "Add a panel first — storyboard exports describe panels": "请先添加面板 — 故事板导出需要面板",
48
+ "ROOT PATH": "根路径",
49
+ "Prompt copied to clipboard": "提示词已复制到剪贴板",
50
+ "Frame export finished. Check your export folder or browser downloads.": "帧导出完成。请查看导出文件夹或浏览器下载。",
51
+ "Redone": "已重做",
52
+ "Undone": "已撤销",
53
+ "Saving…": "保存中…",
54
+ "Save failed": "保存失败",
55
+ "Not saved": "未保存",
56
+ "Unsaved changes": "未保存的更改",
57
+ "Saved": "已保存",
58
+ "Sign in with ChatGPT in the Agent panel to generate a blueprint.": "请在智能体面板中登录 ChatGPT 以生成蓝图。",
59
+ "Sign in to the Agent (⌘B) to sculpt from an image.": "请登录智能体(⌘B)以从图像雕塑。",
60
+ "The live editor is disconnected — reconnect to sculpt.": "实时编辑器已断开 — 请重新连接后再雕塑。",
61
+ "No model is available — add a provider key in the Agent panel.": "没有可用模型 — 请在智能体面板中添加服务商密钥。",
62
+ "New sculpt": "新建雕塑",
63
+ "Add a reference image in the Sculpt panel and press Start — or select a sculpt and press 5.": "在雕塑面板中添加参考图并按开始 — 或选中雕塑并按 5。",
64
+ "The graphics context was lost — restoring the stage. If it stays black, reload the page; your work is autosaved.": "图形上下文已丢失 — 正在恢复舞台。若持续黑屏,请刷新页面;你的工作已自动保存。",
65
+ "Graphics restored.": "图形已恢复。",
66
+ "Dot added — drag it to bend the route": "已添加点 — 拖动它使路径弯曲",
67
+ "3D graphics (WebGL) is turned off in this browser": "此浏览器的 3D 图形(WebGL)已关闭",
68
+ "Quit the browser completely (Cmd+Q) and reopen it, or turn on \"Use graphics acceleration when available\" in its settings. chrome://gpu shows the WebGL status.": "请完全退出浏览器(Cmd+Q)后重新打开,或在设置中开启“尽可能使用图形加速”。chrome://gpu 可查看 WebGL 状态。",
69
+ "The 3D view lost its graphics context": "3D 视图丢失了图形上下文",
70
+ "Waiting for the browser to restore it. If this stays, reload the studio — scenes autosave.": "正在等待浏览器恢复。若持续如此,请刷新工作室 — 场景会自动保存。",
71
+ "Reload": "刷新",
72
+ "Click or ▸ to expand · drag to move": "点击或 ▸ 展开 · 拖动移动",
73
+ "Click or ▾ to fold · drag to move": "点击或 ▾ 折叠 · 拖动移动",
74
+ "Expand inset view": "展开小窗视图",
75
+ "Collapse inset view": "折叠小窗视图",
76
+ "Shot view": "镜头视图",
77
+ "Top-View": "顶视图",
78
+ "Resize inset view": "调整小窗大小",
79
+ "Shot": "镜头",
80
+ "Look through the shot camera": "通过镜头摄影机观察",
81
+ "Look through the shot camera — right-drag, WASD and orbit set the recording lens (Esc returns)": "通过镜头摄影机观察 — 右键拖动、WASD 和环绕设置录制镜头(Esc 返回)",
82
+ "Look through": "镜头视角",
83
+ "Return to the editor view (Esc)": "返回编辑器视图 (Esc)",
84
+ "Shot camera": "镜头摄影机",
85
+ "Esc · exit": "Esc · 退出",
86
+ "SUBJECT OUT OF FRAME": "角色不在画面内",
87
+ "A new version of CozyClay is ready.": "CozyClay 新版本已就绪。",
88
+ "Reload to update": "刷新以更新",
89
+ "Later": "稍后",
90
+ "Object deleted.": "物体已删除。",
91
+ "Undo object deletion": "撤销物体删除",
92
+ "Undo": "撤销",
93
+ "Project access was not granted.": "未获得项目访问权限。",
94
+ "Restore": "恢复",
95
+ "Dismiss": "关闭",
96
+ "Image deleted. This session can undo it.": "图片已删除。本次会话内可撤销。",
97
+ "This account cannot generate images.": "此账号无法生成图像。",
98
+ "Sign in with ChatGPT in the Agent panel first.": "请先在智能体面板中登录 ChatGPT。",
99
+ "unknown error": "未知错误",
100
+ "The image route did not return a PNG.": "图像接口未返回 PNG。",
101
+ "Draft": "草稿",
102
+ "The studio hit a render error": "工作室遇到渲染错误",
103
+ "Scenes autosave as you work, so reloading resumes from the last saved state.": "场景会随工作自动保存,刷新后会从上次保存的状态继续。",
104
+ "Reload the studio": "刷新工作室",
105
+ "Your first 60 seconds": "你的最初 60 秒",
106
+ "Core loop complete": "核心流程完成",
107
+ "Dismiss guide": "关闭指南",
108
+ "You made your first panel.": "你做出了第一个面板。",
109
+ "You made your first shot.": "你做出了第一个镜头。",
110
+ "Describe, review on the Board, then stylize or export. You can close this guide and keep adding panels.": "描述、在看板上检查,然后风格化或导出。你可以关闭本指南并继续添加面板。",
111
+ "Select, move, ask, and play are the core CozyClay loop. You can close this guide and keep blocking.": "选择、移动、提问和播放是 CozyClay 的核心流程。你可以关闭本指南并继续摆位。",
112
+ "Continue editing": "继续编辑",
113
+ "Follow these four actions to see a result quickly.": "跟随这四个步骤,快速看到结果。",
114
+ "Skip guide": "跳过指南",
115
+ "QA clean": "QA 通过",
116
+ "Settings": "设置",
117
+ "Language and analytics": "语言与统计",
118
+ "Medium": "中景",
119
+ "Wide": "广角",
120
+ "Close-Up": "特写",
121
+ "Low Angle": "低角度",
122
+ "High Angle": "高角度",
123
+ "Saved scenes were written by a newer CozyClay — they have been left untouched and this session will not save": "已保存的场景由更新版本的 CozyClay 创建 — 已保持原样,本次会话也不会保存",
124
+ };