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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"desktop:dist:win": "npm run build && npm run desktop:build:main && npm run desktop:prebundle && tsx desktop/prepare-target.mts win32-x64 && node -e \"const {spawnSync}=require('node:child_process');const result=spawnSync(process.execPath,['node_modules/electron-builder/cli.js','--config','config/electron-builder.config.mjs','--win','nsis','--x64','--publish','never'],{stdio:'inherit',env:{...process.env,CC_EB_TARGET:'win32-x64'}});if(result.error)throw result.error;process.exit(result.status??1)\"",
"desktop:dist:linux": "npm run build && npm run desktop:build:main && npm run desktop:prebundle && tsx desktop/prepare-target.mts linux-x64 && CC_EB_TARGET=linux-x64 electron-builder --config config/electron-builder.config.mjs --linux AppImage --x64 --publish never",
"verify:shortcuts": "tsx src/shortcuts/catalog.verify.ts && tsx src/shortcuts/keymap.verify.ts && tsx src/shortcuts/shortcutScope.verify.ts && tsx src/shortcuts/native-copy.verify.ts",
"verify:i18n": "node scripts/check-i18n.mjs && tsx src/i18n/locale.verify.ts && tsx src/i18n/dict/it/settingsCoverage.verify.ts && tsx src/i18n/dict/it/mediaCoverage.verify.ts && tsx src/components/settings/transcription-language.verify.ts",
"verify:i18n": "node scripts/check-i18n.mjs && tsx src/i18n/locale.verify.ts && tsx src/i18n/dict/it/settingsCoverage.verify.ts && tsx src/i18n/dict/it/mediaCoverage.verify.ts && tsx src/i18n/dict/ko/coverage.verify.ts && tsx src/components/settings/transcription-language.verify.ts",
"verify:editor": "tsx src/editor/storeCommandBuilder.verify.ts && tsx src/editor/timelineFrameRate.verify.ts && tsx src/editor/projectFrameRate.verify.ts && tsx src/editor/keyframes.verify.ts && tsx src/editor/flexCrop.verify.ts && tsx src/editor/multiSelect.verify.ts && tsx src/editor/professionalTimeline.verify.ts && tsx src/editor/proposal.verify.ts && tsx src/editor/ripple-fade.verify.ts && tsx src/editor/ripple.verify.ts && tsx src/editor/split.verify.ts && tsx src/editor/transitionPreview.verify.ts && tsx src/editor/captionReferences.verify.ts && tsx src/editor/zoom.verify.ts && tsx src/editor/keyframes-check.verify.ts && tsx src/editor/audio-transcript-guard.verify.ts && tsx src/editor/caption-overlay-visibility.verify.ts && tsx src/editor/motionGraphicTime.verify.ts",
"verify:captions": "tsx src/captions/captionCueMenu.verify.ts && tsx src/captions/captionIdentity.verify.ts && tsx src/captions/exportCaptions.verify.ts && tsx src/captions/presetStore.verify.ts && tsx src/captions/renderStyles.verify.ts && tsx src/captions/segmenter.verify.ts && tsx src/captions/styles.verify.ts && tsx src/transcript/edit.verify.ts && tsx src/transcript/transcribe-jobs.verify.ts && tsx src/transcript/variants.verify.ts && tsx src/audio/loudness.verify.ts && tsx src/audio/vad.verify.ts && tsx src/audio/vadPreference.verify.ts && tsx src/transcript/transcribe-jobs-check.verify.ts && tsx src/audio/loudnessBatch.verify.ts && tsx src/transcript/projection.verify.ts",
"verify:agent-tools": "tsx src/agent/external-edit-session-apply.verify.ts && tsx src/agent/external-edit-session-cancellation.verify.ts && tsx src/agent/external-edit-session-core.verify.ts && tsx src/agent/external-edit-session-projection.verify.ts && tsx src/agent/external-edit-session-runtime.verify.ts && tsx src/agent/external-tool-policy.verify.ts && tsx src/agent/progress/job-model.verify.ts && tsx src/agent/progress/track-progress-targets.verify.ts && tsx src/agent/selection-refs.verify.ts && tsx src/agent/skills/plugin-skills.verify.ts && tsx src/agent/skills/skills-catalog.verify.ts && tsx src/agent/tools/agent-gap-p3.verify.ts && tsx src/agent/tools/audio-assets.verify.ts && tsx src/agent/tools/auto-grade-tools.verify.ts && tsx src/agent/tools/captions-lanes.verify.ts && tsx src/agent/tools/captions-tools.verify.ts && tsx src/agent/tools/design-tools.verify.ts && tsx src/agent/tools/edit-asset-tools.verify.ts && tsx src/agent/tools/edit-item-authored.verify.ts && tsx src/agent/tools/edit-item-generic.verify.ts && tsx src/agent/tools/edit-item-source-range.verify.ts && tsx src/agent/tools/edit-item-pool-replacement.verify.ts && tsx src/agent/tools/timeline-import-tools.verify.ts && tsx src/agent/tools/timeline-import-fcpxml.verify.ts && tsx src/agent/tools/timeline-import-edl.verify.ts && tsx src/agent/tools/edit-item-volume-hint.verify.ts && tsx src/agent/tools/export-tools.verify.ts && tsx src/agent/tools/followup-tools.verify.ts && tsx src/agent/tools/font-tools.verify.ts && tsx src/agent/tools/frames-tool.verify.ts && tsx src/agent/tools/renderSnapshotMedia.verify.ts && tsx src/agent/tools/friction-tools.verify.ts && tsx src/agent/tools/generate-rerun.verify.ts && tsx src/agent/tools/highlight-tool.verify.ts && tsx src/agent/tools/markers-tools.verify.ts && tsx src/agent/tools/media-pool-favorite-delete.verify.ts && tsx src/agent/tools/media-pool-relink.verify.ts && tsx src/agent/tools/media-pool-tools.verify.ts && tsx src/agent/tools/mg-code-tools.verify.ts && tsx src/agent/tools/mg-video-tools.verify.ts && tsx src/agent/tools/probe-tools.verify.ts && tsx src/agent/toolFailureNote.verify.ts && tsx src/agent/tools/project-tools.verify.ts && tsx src/agent/tools/followup-tools-check.verify.ts && tsx src/agent/tools/probe-tools-check.verify.ts && tsx src/agent/tools/read-project-tools-check.verify.ts && tsx src/agent/tools/stock-tools-check.verify.ts && tsx src/agent/tools/stock-tools-probe.verify.ts && tsx src/agent/tools/template-tools-check.verify.ts && node scripts/run-check.mjs src/agent/tools/effect-tools.verify.ts && node scripts/run-check.mjs src/agent/tools/library-edit-item.verify.ts && tsx src/agent/tools/read-project-tools.verify.ts && tsx src/agent/tools/shader-tools.verify.ts && tsx src/agent/tools/skill-tools.verify.ts && tsx src/agent/tools/transcript-clear-edits.verify.ts && tsx src/agent/tools/transcript-tools.verify.ts && tsx src/agent/tools/version-tools.verify.ts && tsx src/agent/tools/web-tools.verify.ts",
Expand Down
1 change: 1 addition & 0 deletions scripts/verify-gate-coverage.verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ assert.equal(renderStep['continue-on-error'], undefined);
const dataModules = new Set([
'src/i18n/dict/ru/index.ts', // Translation dictionary data, no application logic.
'src/i18n/dict/en/settings.ts', // Translation dictionary data, no application logic.
'src/i18n/dict/ko/settings.ts', // Translation dictionary data, no application logic.
]);
const sourceFiles = [...new Set(execFileSync('git', [
'ls-files', '--cached', '--others', '--exclude-standard', '-z',
Expand Down
8 changes: 7 additions & 1 deletion server/mobile-upload-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ interface MobileUploadSession extends MobileUploadSessionSnapshot {
activeUploads: Set<Promise<void>>;
}

export type MobilePageLocale = 'zh' | 'en' | 'it' | 'ru';
export type MobilePageLocale = 'zh' | 'en' | 'it' | 'ru' | 'ko';

interface MobileUploadServiceOptions {
bindHost?: string;
Expand Down Expand Up @@ -172,6 +172,12 @@ function mobilePage(locale: MobilePageLocale): string {
choose: '选择素材', multiple: '支持多选,页面保持打开直到全部完成',
waiting: '等待上传', sent: '已发送', failed: '上传失败', interrupted: '网络中断',
},
ko: {
pageTitle: '휴대폰에서 업로드', title: 'OpenChatCut으로 미디어 보내기',
hint: '휴대폰에서 동영상, 이미지 또는 오디오를 선택하세요. 두 기기를 같은 로컬 네트워크에 연결해 두세요.',
choose: '미디어 선택', multiple: '여러 파일을 선택할 수 있습니다. 모든 업로드가 끝날 때까지 이 페이지를 열어 두세요.',
waiting: '업로드 대기 중', sent: '전송됨', failed: '업로드 실패', interrupted: '네트워크 연결 끊김',
},
}[copyLocale];
const scriptCopy = JSON.stringify({ waiting: copy.waiting, sent: copy.sent, failed: copy.failed, interrupted: copy.interrupted });
return `<!doctype html>
Expand Down
5 changes: 5 additions & 0 deletions server/mobile-upload-service.verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ try {
assert.match(await (await fetch(italianSession.urls[0]!)).text(), /Send media to OpenChatCut/);
const russianSession = await service.createSession('ru');
assert.match(await (await fetch(russianSession.urls[0]!)).text(), /Отправить медиафайлы в OpenChatCut/);
const koreanSession = await service.createSession('ko');
const koreanPage = await (await fetch(koreanSession.urls[0]!)).text();
assert.match(koreanPage, /<html lang="ko">/);
assert.match(koreanPage, /OpenChatCut으로 미디어 보내기/);

const tooLarge = await fetch(`${session.urls[0]}/upload?name=large.mp4`, {
method: 'POST',
Expand All @@ -99,6 +103,7 @@ try {
assert.equal(service.getSession(englishSession.id), null);
assert.equal(service.getSession(italianSession.id), null);
assert.equal(service.getSession(russianSession.id), null);
assert.equal(service.getSession(koreanSession.id), null);
} finally {
await service.stop();
await rm(tempDir, { recursive: true, force: true });
Expand Down
6 changes: 3 additions & 3 deletions server/plugins/mobile-upload.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { Plugin } from 'vite';
import { editorCredentialAuthorized, trustedEditorRequest } from '../editor-auth.ts';
import { MobileUploadService } from '../mobile-upload-service.ts';
import { MobileUploadService, type MobilePageLocale } from '../mobile-upload-service.ts';
import { putUploadFile } from '../r2.ts';
import { maxUploadBytes } from './upload.ts';

Expand All @@ -14,8 +14,8 @@ function sendJson(res: ServerResponse, status: number, body: unknown): void {

type MobileUploadControls = Pick<MobileUploadService, 'createSession' | 'getSession' | 'closeSession'>;

function mobilePageLocale(value: string | null): 'zh' | 'en' | 'it' | 'ru' {
return value === 'en' || value === 'it' || value === 'ru' ? value : 'zh';
function mobilePageLocale(value: string | null): MobilePageLocale {
return value === 'en' || value === 'it' || value === 'ru' || value === 'ko' ? value : 'zh';
}

function mobileUploadControlAuthorized(req: IncomingMessage, res: ServerResponse): boolean {
Expand Down
10 changes: 7 additions & 3 deletions server/plugins/mobile-upload.verify.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import type { MobileUploadSessionSnapshot } from '../mobile-upload-service';
import type { MobilePageLocale, MobileUploadSessionSnapshot } from '../mobile-upload-service';
import { handleMobileUploadControl } from './mobile-upload';

const snapshot: MobileUploadSessionSnapshot = {
Expand All @@ -14,7 +14,7 @@ let reads = 0;
let deletes = 0;
const createdLocales: string[] = [];
const controls = {
async createSession(locale: 'zh' | 'en' | 'it' | 'ru' = 'zh') {
async createSession(locale: MobilePageLocale = 'zh') {
creates += 1;
createdLocales.push(locale);
return snapshot;
Expand Down Expand Up @@ -107,7 +107,11 @@ try {
method: 'POST', headers: { origin },
});
assert.equal(italian.status, 201);
assert.deepEqual(createdLocales, ['en', 'ru', 'it']);
const korean = await fetch(`${origin}/sessions?locale=ko`, {
method: 'POST', headers: { origin },
});
assert.equal(korean.status, 201);
assert.deepEqual(createdLocales, ['en', 'ru', 'it', 'ko']);
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
Expand Down
9 changes: 5 additions & 4 deletions server/ui-locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
// here instead of guessing. Unset means the app's original Chinese.
import { getKey } from './keystore.ts';

export type UiLocale = 'zh' | 'en' | 'it' | 'ru';
export type UiLocale = 'zh' | 'en' | 'it' | 'ru' | 'ko';

const LOCALES: ReadonlySet<string> = new Set(['zh', 'en', 'it', 'ru']);
const LOCALES: ReadonlySet<string> = new Set(['zh', 'en', 'it', 'ru', 'ko']);

export function parseUiLocale(value: unknown): UiLocale | null {
const tag = typeof value === 'string' ? value.trim().toLowerCase() : '';
Expand All @@ -20,10 +20,11 @@ export function uiLocale(): UiLocale {
return parseUiLocale(getKey('UI_LOCALE')) ?? 'zh';
}

/** Pick the variant for the current interface language; Italian falls back to English when not provided. */
export function localized<T>(variants: { zh: T; en: T; ru?: T; it?: T }, locale: UiLocale = uiLocale()): T {
/** Pick the variant for the current interface language; Italian, Russian and Korean fall back to English when not provided. */
export function localized<T>(variants: { zh: T; en: T; ru?: T; it?: T; ko?: T }, locale: UiLocale = uiLocale()): T {
if (locale === 'zh') return variants.zh;
if (locale === 'ru') return variants.ru ?? variants.en;
if (locale === 'it') return variants.it ?? variants.en;
if (locale === 'ko') return variants.ko ?? variants.en;
return variants.en;
}
2 changes: 2 additions & 0 deletions src/agent/systemPromptOrder.verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const commonPrefixLength = (a: string, b: string): number => {
assert.match(agentLanguagePrompt('it'), /in Italian/);
assert.match(agentLanguagePrompt('ru'), /interface language is Russian/);
assert.match(agentLanguagePrompt('ru'), /in Russian/);
assert.match(agentLanguagePrompt('ko'), /interface language is Korean/);
assert.match(agentLanguagePrompt('ko'), /in Korean/);
}

// ── Public product identity must override stale names in workflows or conversation memory ──
Expand Down
5 changes: 3 additions & 2 deletions src/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const McpGuideDialog = lazy(() => loadMcpGuideDialog().then((m) => ({ default: m

// Language switching: The text pill displays the current language; clicking
// cycles through the supported locales. First run defaults to the
// system language (or English) — see i18n/locale.ts.
// system language (or English) — see i18n/locale.ts. Chinese and Korean show
// their own script; the others show the locale code.
export function LocaleToggle() {
const t = useT();
const locale = getLocale();
Expand All @@ -32,7 +33,7 @@ export function LocaleToggle() {
style={{ minWidth: 30, height: 22, background: 'none', border: `0.5px solid ${theme.border}`, borderRadius: 4, cursor: 'pointer', padding: '0 5px', fontSize: 11, fontWeight: 600, letterSpacing: 0.3, color: theme.textDim, display: 'grid', placeItems: 'center' }}
onMouseEnter={(e) => { e.currentTarget.style.color = theme.text; e.currentTarget.style.background = theme.panelAlt; }}
onMouseLeave={(e) => { e.currentTarget.style.color = theme.textDim; e.currentTarget.style.background = 'none'; }}>
{locale === 'zh' ? '中' : locale.toUpperCase()}
{locale === 'zh' ? '中' : locale === 'ko' ? '한' : locale.toUpperCase()}
</button>
);
}
Expand Down
55 changes: 55 additions & 0 deletions src/i18n/dict/ko/audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// KO dictionary (field fragmentation mirroring dict/en, key = Chinese original text).
export default {
'此浏览器不支持录音': '이 브라우저는 녹음을 지원하지 않습니다',
'麦克风权限被拒绝': '마이크 권한이 거부되었습니다',
'无法访问麦克风': '마이크에 접근할 수 없습니다',
'UI 与动效反馈': 'UI 및 모션 피드백',
'转场与强调': '전환 및 강조',
'设备与质感': '기기 및 질감',
'反应与情绪': '반응 및 분위기',
'音乐分析': '음악 분석',
'音乐分析:{name},{status}': '음악 분석: {name}, {status}',
'{name} 的音乐分析': '{name} 음악 분석',
'检查模型': '모델 확인 중',
'模型未安装': '모델이 설치되지 않음',
'分析音乐': '음악 분석',
'重新分析': '다시 분석',
'重试分析': '분석 재시도',
'排队中': '대기 중',
'分析失败': '분석 실패',
'解码音频': '오디오 디코딩 중',
'分析节拍': '리듬 분석 중',
'理解音乐内容': '음악 이해 중',
'已加入本地分析队列。': '로컬 분석 대기열에 추가되었습니다.',
'分析失败:{message}': '분석 실패: {message}',
'尚未安装所需模型包。请在本地 AI 设置中分别下载节拍与音乐语义模型;这里不会自动下载。': '필요한 모델 팩이 설치되어 있지 않습니다. 로컬 AI 설정에서 리듬 모델과 음악 의미 분석 모델을 각각 다운로드하세요. 여기서는 자동으로 다운로드하지 않습니다.',
'已显示缓存结果;重新分析需要先安装模型包。': '캐시된 결과를 표시하고 있습니다. 다시 분석하려면 먼저 모델 팩을 설치하세요.',
'正在检查本地模型…': '로컬 모델 확인 중…',
'在本机分析节拍、结构和音乐语义。音频不会上传。': '이 기기에서 리듬, 구조, 음악적 의미를 분석합니다. 오디오는 업로드되지 않습니다.',
'速度': '속도',
'拍号': '박자',
'置信度': '신뢰도',
'节奏详情': '리듬 세부 정보',
'{beats} 个节拍 · {downbeats} 个重拍': '비트 {beats}개 · 다운비트 {downbeats}개',
'节拍时间点': '비트 타임스탬프',
'重拍时间点': '다운비트 타임스탬프',
'另有 {count} 个时间点': '타임스탬프 {count}개 더 있음',
'音乐标签': '음악 태그',
'流派': '장르',
'情绪': '분위기',
'乐器': '악기',
'用途': '용도',
'未知': '알 수 없음',
'段落': '구간',
'能量': '에너지',
'边界': '경계',
'未识别到明确段落': '뚜렷한 구간이 감지되지 않았습니다',
'前奏': '인트로',
'推进': '빌드업',
'高潮': '절정',
'间奏': '브레이크',
'平稳': '안정',
'尾奏': '아웃트로',
'自动分析新导入的音视频': '새로 가져온 오디오 및 비디오 자동 분석',
'需要先在本地 AI 设置中单独下载模型包;开启此选项不会自动下载。': '먼저 로컬 AI 설정에서 모델 팩을 별도로 다운로드하세요. 이 옵션을 켜도 다운로드가 시작되지 않습니다.',
} as Record<string, string>;
Loading