Ask which language the content is written in

The Instruction agent now confirms the output language before the plan is
ready: Bahasa Indonesia, English or 中文, the languages the engine writes
in. It can also be named in the instruction ("pakai bahasa Inggris"), is
read from short replies, and can be changed in the plan. Research alone
is not asked about.

The engine's content production has no language field, so the choice
goes into the agent's instructions; video schedules also set
video_options.language. ruleOf reads it back, so editing a topic keeps
it. The audio player recognises Chinese scripts and uses a zh-CN voice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-23 11:32:06 +07:00
parent 2b37d80e26
commit f55cccb6eb
9 changed files with 164 additions and 31 deletions

View File

@@ -4,7 +4,10 @@
import { opKey, request } from '../api/client'
import { listDocuments, type Agent, type RunSchedule } from '../api/engine'
import { getLang } from '../i18n'
import { KINDS, SCHEDULABLE, videoCadence, weeklyTarget, type ContentKind, type ContentRule, type Per, type ResearchRule, type Rule } from './parse'
import {
KINDS, LANGUAGES, SCHEDULABLE, videoCadence, weeklyTarget,
type ContentKind, type ContentRule, type OutputLanguage, type Per, type ResearchRule, type Rule,
} from './parse'
const AGENTS = '/organizations/current/agents'
export const PREFIX = 'Express · '
@@ -25,7 +28,7 @@ export interface VideoOptions {
max_scenes: number
max_duration_seconds: number
art_style: 'realistic' | 'cartoon' | 'anime' | 'watercolor' | '3d'
language: 'English' | 'Bahasa Indonesia'
language: OutputLanguage
}
export interface ExpressAgent extends Agent {
@@ -56,11 +59,24 @@ function scheduleName(rule: Rule): string {
const isKind = (k: string | undefined): k is ContentKind => KINDS.includes(k as ContentKind)
/** A rule with no language chosen is written in the UI language, as before languages could be chosen. */
export const defaultLanguage = (): OutputLanguage => (getLang() === 'id' ? 'Bahasa Indonesia' : 'English')
// How the agent's English brief names each language; read back by languageOfAgent.
const LANGUAGE_TEXT: Record<OutputLanguage, string> = {
English: 'English', 'Bahasa Indonesia': 'Bahasa Indonesia', 中文: 'Simplified Chinese (中文)',
}
const languageSentence = (l: OutputLanguage) => `Write all content in ${LANGUAGE_TEXT[l]}.`
function languageOfAgent(agent: ExpressAgent): OutputLanguage | undefined {
return agent.video_options?.language ?? LANGUAGES.find(l => agent.instructions?.includes(languageSentence(l)))
}
export function ruleOf(agent: ExpressAgent): Rule {
const topic = agent.name.split(' · ').slice(2).join(' · ')
if (agent.enabled_tools.includes('studio.create_video')) {
const daily = agent.run_schedule?.cadence === 'daily'
return { type: 'content', kind: 'video', count: 1, per: daily ? 'day' : 'week', topic }
return { type: 'content', kind: 'video', count: 1, per: daily ? 'day' : 'week', topic, language: languageOfAgent(agent) }
}
if (agent.content_goal?.enabled && agent.material_outputs[0]) {
// " per hari" comes from schedules created before names were in English.
@@ -69,7 +85,7 @@ export function ruleOf(agent: ExpressAgent): Rule {
// Audio and SCORM are Express kinds made of Compose outputs, so the name carries the kind.
const named = agent.name.match(/^Express · \d+ (\w+) per /)?.[1]
const kind = isKind(named) ? named : agent.material_outputs[0].kind
return { type: 'content', kind, per, count: per === 'day' ? Math.max(1, Math.round(target / 7)) : target, topic }
return { type: 'content', kind, per, count: per === 'day' ? Math.max(1, Math.round(target / 7)) : target, topic, language: languageOfAgent(agent) }
}
const s = agent.run_schedule ?? { cadence: 'off' }
return { type: 'research', cadence: s.cadence === 'weekly' ? 'weekly' : 'daily', weekday: s.weekday ?? 0, topic, autoApprove: agent.auto_approve_sources }
@@ -135,7 +151,8 @@ function outputsFor(kind: ContentKind): ExpressAgent['material_outputs'] {
return [{ kind, options: {} }]
}
// The agent's brief is in English; the content language follows the UI language at creation time.
// The agent's brief is in English; the content language is the one chosen in the Instruction chat.
// The engine's content production has no language field, so it is part of the instructions.
function goalText(rule: Rule): string {
const about = rule.topic ? ` about ${rule.topic}` : ''
if (rule.type === 'research') {
@@ -145,7 +162,7 @@ function goalText(rule: Rule): string {
}
function instructionsFor(rule: Rule): string {
const language = getLang() === 'id' ? 'Write all content in Bahasa Indonesia.' : 'Write all content in English.'
const language = languageSentence((rule.type === 'content' && rule.language) || defaultLanguage())
const brief = rule.type === 'content' ? KIND_BRIEF[rule.kind] : undefined
return [rule.topic && `Focus on the topic: ${rule.topic}.`, brief, language].filter(Boolean).join(' ')
}
@@ -178,7 +195,7 @@ export function configFor(rule: Rule, videoCredits = 0) {
max_scenes: VIDEO.scenes,
max_duration_seconds: VIDEO.seconds,
art_style: 'realistic',
language: getLang() === 'id' ? 'Bahasa Indonesia' : 'English',
language: rule.language ?? defaultLanguage(),
} satisfies VideoOptions,
run_schedule: { cadence: videoCadence(rule) ?? 'weekly', hour: 9, minute: 0, weekday: 0 } satisfies RunSchedule,
}