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

@@ -45,6 +45,26 @@ const WEEKDAYS: [RegExp, number][] = [
export type Per = 'day' | 'week'
// The output languages the engine writes in (generation_brief.OutputLanguage, AgentVideoOptions.language).
export type OutputLanguage = 'English' | 'Bahasa Indonesia' | '中文'
export const LANGUAGES: OutputLanguage[] = ['Bahasa Indonesia', 'English', '中文']
export const languageLabel = (l: OutputLanguage) => (l === '中文' ? '中文 (Mandarin)' : l)
// "in English", "pakai bahasa Inggris", "dalam bahasa Mandarin". A bare country name needs "bahasa",
// so a topic such as "sales in China" is not read as a language.
const LANGUAGE_CORE = String.raw`bahasa\s+(?:indonesia|indo|inggris|english|mandarin|cina|china|tionghoa)|indonesian|english|inggris|mandarin|chinese|中文`
const LANGUAGE_PHRASE = new RegExp(
String.raw`(?<!\p{L})(?:(?:in|dalam|pakai|pake|memakai|menggunakan|gunakan|using|use|with|berbahasa)\s+)?(?:the\s+)?(?:${LANGUAGE_CORE})(?:\s+language)?(?!\p{L})`,
'gu',
)
/** The output language a text names, if any. */
export function languageOf(text: string): OutputLanguage | null {
const m = text.toLowerCase().match(LANGUAGE_PHRASE)?.[0]
if (!m) return null
return /indo/.test(m) ? 'Bahasa Indonesia' : /english|inggris/.test(m) ? 'English' : '中文'
}
export interface ContentRule {
type: 'content'
kind: ContentKind
@@ -53,6 +73,8 @@ export interface ContentRule {
topic: string
/** The monthly count the instruction asked for, when it had to be turned into a weekly one. */
monthly?: number
/** The language the content is written in; unset means the UI language. */
language?: OutputLanguage
}
export interface ResearchRule {
@@ -71,6 +93,8 @@ export interface ParseResult {
rules: Rule[]
/** Clauses that said something but matched no rule, so the user can rephrase them. */
unknown: string[]
/** The output language the instruction named. */
language?: OutputLanguage
}
const TOPIC = /\b(?:soal|tentang|mengenai|seputar|terkait|topik|bertema|perihal|about|regarding|covering|related to|on the topic of|topic)\s+(?!latihan\b)(.+)$/
@@ -128,8 +152,10 @@ function parseClause(clause: string): Rule | null {
}
export function parseInstruction(text: string): ParseResult {
const language = languageOf(text)
const clauses = text
.toLowerCase()
.replace(LANGUAGE_PHRASE, ' ')
.replace(/[“”"]/g, '')
.split(SPLIT)
.map(s => s.trim())
@@ -150,7 +176,7 @@ export function parseInstruction(text: string): ParseResult {
const topics = new Set(rules.map(r => r.topic).filter(Boolean))
const fallback = shared || (topics.size === 1 ? [...topics][0] : '')
for (const r of rules) if (!r.topic) r.topic = fallback
return { rules, unknown }
return language ? { rules, unknown, language } : { rules, unknown }
}
export function weeklyTarget(rule: ContentRule): number {