feat: complete kouyu_english app codebase, A0 specifications and .gitignore
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import 'models.dart';
|
||||
import 'writing_feedback.dart';
|
||||
|
||||
class AssessmentTask {
|
||||
const AssessmentTask({
|
||||
required this.id,
|
||||
required this.skill,
|
||||
required this.prompt,
|
||||
this.audio,
|
||||
this.choices = const [],
|
||||
this.answerIndex,
|
||||
this.requiredWords = const [],
|
||||
required this.abilityId,
|
||||
});
|
||||
final String id;
|
||||
final AssessmentSkill skill;
|
||||
final String prompt;
|
||||
final String? audio;
|
||||
final List<String> choices;
|
||||
final int? answerIndex;
|
||||
final List<String> requiredWords;
|
||||
final String abilityId;
|
||||
}
|
||||
|
||||
class AssessmentPack {
|
||||
const AssessmentPack({required this.id, required this.tasks});
|
||||
final String id;
|
||||
final List<AssessmentTask> tasks;
|
||||
List<AssessmentTask> forSkill(AssessmentSkill skill) =>
|
||||
tasks.where((task) => task.skill == skill).toList();
|
||||
}
|
||||
|
||||
final a0AssessmentPacks = [
|
||||
AssessmentPack(
|
||||
id: 'A0-E1',
|
||||
tasks: _tasks(
|
||||
'A',
|
||||
const [
|
||||
'Hello. I’m Mia.|自我介绍|买东西|问时间|0',
|
||||
'How do you spell your name?|问姓名拼写|问地点|问时间|0',
|
||||
'I’m tired today.|很好|累|来自哪里|1',
|
||||
'seven|6|7|8|1',
|
||||
'one-three-eight|138|183|318|0',
|
||||
'It’s a key.|书|钥匙|包|1',
|
||||
'I’m from Hong Kong.|香港|伦敦|北京|0',
|
||||
'This is my mother.|妈妈|姐姐|朋友|0',
|
||||
'It is Monday.|星期一|星期二|星期三|0',
|
||||
'I like tea.|茶|咖啡|电影|0',
|
||||
],
|
||||
const [
|
||||
'Mia: I’m from Hong Kong.\nShen: I’m good.\nMia: My number is one-three-eight.|Mia 来自哪里?|Hong Kong|London|0',
|
||||
'Mia: It is Monday.\nShen: It is three o’clock.|几点?|three|one|0',
|
||||
'Mia: This is my brother.\nShen: Nice to meet you.|谁是家人?|brother|mother|0',
|
||||
'Mia: I like music.\nShen: I like tea.|Shen 喜欢什么?|tea|music|0',
|
||||
'Mia: What’s this?\nShen: It’s a pen.|这是什么?|pen|bag|0',
|
||||
],
|
||||
const [
|
||||
'用英语介绍自己。|i,m',
|
||||
'用英语说明你来自哪里。|i,m,from',
|
||||
'用英语说一个喜好。|i,like',
|
||||
'用英语说“这是一支笔”。|it,s,pen',
|
||||
'用英语说明星期或整点。|it,s',
|
||||
],
|
||||
const [
|
||||
'介绍姓名、地点、状态/喜好并反问。|hello,i,m,from',
|
||||
'完整拼读一个四字母姓名。|a,l,e,x',
|
||||
'说一个三位虚拟号码。|one,three,eight',
|
||||
'介绍一位家人。|this,my',
|
||||
'说明星期。|it,s,monday',
|
||||
'说明整点。|it,s,o,clock',
|
||||
'请求对方重复或放慢。|please',
|
||||
'说出一个物品。|it,s',
|
||||
],
|
||||
),
|
||||
),
|
||||
AssessmentPack(
|
||||
id: 'A0-E2',
|
||||
tasks: _tasks(
|
||||
'B',
|
||||
const [
|
||||
'Hi. My name is Leo.|自我介绍|买东西|问号码|0',
|
||||
'How do you spell that?|问姓名拼写|问喜好|问家人|0',
|
||||
'I’m okay.|很好|还可以|累|1',
|
||||
'four|3|4|5|1',
|
||||
'two-nine-six|296|269|926|0',
|
||||
'It’s a bag.|笔|包|水|1',
|
||||
'I’m from London.|伦敦|香港|北京|0',
|
||||
'This is my sister.|姐姐|妈妈|朋友|0',
|
||||
'It is Friday.|星期五|星期一|星期日|0',
|
||||
'I like coffee.|茶|咖啡|音乐|1',
|
||||
],
|
||||
const [
|
||||
'Mia: I’m from London.\nLeo: I’m okay.\nMia: My number is two-nine-six.|号码是多少?|296|269|0',
|
||||
'Mia: It is Friday.\nLeo: It is six o’clock.|几点?|six|three|0',
|
||||
'Mia: This is my father.\nLeo: Hi!|谁是家人?|father|brother|0',
|
||||
'Mia: I like movies.\nLeo: I like coffee.|Leo 喜欢什么?|coffee|movies|0',
|
||||
'Mia: What’s this?\nLeo: It’s water.|这是什么?|water|book|0',
|
||||
],
|
||||
const [
|
||||
'用英语介绍自己。|my,name,is',
|
||||
'用英语说明你来自哪里。|i,m,from',
|
||||
'用英语说一个喜好。|i,like',
|
||||
'用英语说“这是一个包”。|it,s,bag',
|
||||
'用英语说明星期或整点。|it,s',
|
||||
],
|
||||
const [
|
||||
'介绍姓名、地点、状态/喜好并反问。|hi,i,m,from',
|
||||
'完整拼读一个四字母姓名。|l,e,o,n',
|
||||
'说一个三位虚拟号码。|two,nine,six',
|
||||
'介绍一位家人。|this,my',
|
||||
'说明星期。|it,s,friday',
|
||||
'说明整点。|it,s,o,clock',
|
||||
'请求对方重复或放慢。|please',
|
||||
'说出一个物品。|it,s',
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
/// Replacement packs use different people, places, numbers, days and likes.
|
||||
/// They keep the same assessed ability but are not immediate replays.
|
||||
final a0ReplacementPacks = [
|
||||
_replacementPack(a0AssessmentPacks[0], 'A0-E1R', const {
|
||||
'Mia': 'Nora',
|
||||
'Shen': 'Kai',
|
||||
'Hong Kong': 'London',
|
||||
'Monday': 'Friday',
|
||||
'one-three-eight': 'two-four-seven',
|
||||
'138': '247',
|
||||
'three o’clock': 'six o’clock',
|
||||
'tea': 'coffee',
|
||||
'key': 'bag',
|
||||
'mother': 'father',
|
||||
}),
|
||||
_replacementPack(a0AssessmentPacks[1], 'A0-E2R', const {
|
||||
'Mia': 'Ava',
|
||||
'Leo': 'Bo',
|
||||
'London': 'Hong Kong',
|
||||
'Friday': 'Tuesday',
|
||||
'two-nine-six': 'five-one-four',
|
||||
'296': '514',
|
||||
'six o’clock': 'two o’clock',
|
||||
'coffee': 'music',
|
||||
'bag': 'book',
|
||||
'sister': 'brother',
|
||||
}),
|
||||
];
|
||||
|
||||
AssessmentPack? replacementFor(String packId) => switch (packId) {
|
||||
'A0-E1' => a0ReplacementPacks[0],
|
||||
'A0-E2' => a0ReplacementPacks[1],
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// Local checks for the frozen A0 exit tasks. They accept variable names and
|
||||
/// places, but require the communicative information named by each task.
|
||||
bool checkOpenAssessmentAnswer(AssessmentTask task, String input) {
|
||||
final text = input.toLowerCase().replaceAll('’', "'");
|
||||
final words = RegExp(
|
||||
r"[a-z]+(?:'[a-z]+)?",
|
||||
).allMatches(text).map((match) => match.group(0)!).toSet();
|
||||
bool has(String word) => words.contains(word);
|
||||
bool phrase(String value) => text.contains(value);
|
||||
bool hasAny(Iterable<String> values) => values.any(has);
|
||||
final introduction = phrase("i'm") || phrase('i am') || phrase('my name is');
|
||||
final itIs = phrase("it's") || phrase('it is');
|
||||
final numberWords = RegExp(
|
||||
r'\b(zero|one|two|three|four|five|six|seven|eight|nine|ten)\b',
|
||||
).allMatches(text).length;
|
||||
final position = int.tryParse(
|
||||
RegExp(r'(\d+)$').firstMatch(task.id)?.group(1) ?? '',
|
||||
);
|
||||
if (position == null) return false;
|
||||
|
||||
if (task.skill == AssessmentSkill.writing) {
|
||||
const lessonIds = ['a0-01', 'a0-06', 'a0-09', 'a0-05', 'a0-08'];
|
||||
return position >= 1 &&
|
||||
position <= lessonIds.length &&
|
||||
WritingFeedback.check(lessonIds[position - 1], input).complete;
|
||||
}
|
||||
if (task.skill != AssessmentSkill.speaking) return false;
|
||||
return switch (position) {
|
||||
1 =>
|
||||
hasAny(['hello', 'hi']) &&
|
||||
introduction &&
|
||||
has('from') &&
|
||||
(has('like') || hasAny(['good', 'okay', 'tired'])) &&
|
||||
hasAny(['what', 'where', 'how', 'do']),
|
||||
2 => RegExp(r'[a-z](?:[ -]?[a-z]){3,}').hasMatch(text),
|
||||
3 => numberWords >= 3,
|
||||
4 =>
|
||||
phrase('this is my') && hasAny(['mother', 'father', 'sister', 'brother']),
|
||||
5 =>
|
||||
itIs &&
|
||||
hasAny([
|
||||
'monday',
|
||||
'tuesday',
|
||||
'wednesday',
|
||||
'thursday',
|
||||
'friday',
|
||||
'saturday',
|
||||
'sunday',
|
||||
]),
|
||||
6 =>
|
||||
itIs &&
|
||||
(has('clock') ||
|
||||
has("o'clock") ||
|
||||
has('oclock') ||
|
||||
phrase("o'clock")),
|
||||
7 => has('please') && hasAny(['say', 'speak', 'repeat', 'slow', 'slowly']),
|
||||
8 => itIs && hasAny(['book', 'pen', 'bag', 'key', 'water']),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
AssessmentPack _replacementPack(
|
||||
AssessmentPack source,
|
||||
String id,
|
||||
Map<String, String> values,
|
||||
) {
|
||||
String replace(String value) {
|
||||
var result = value;
|
||||
for (final entry in values.entries) {
|
||||
result = result.replaceAll(entry.key, entry.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return AssessmentPack(
|
||||
id: id,
|
||||
tasks: source.tasks
|
||||
.map(
|
||||
(task) => AssessmentTask(
|
||||
id: task.id.replaceFirst(
|
||||
source.id.split('-').last,
|
||||
id.split('-').last,
|
||||
),
|
||||
skill: task.skill,
|
||||
prompt: replace(task.prompt),
|
||||
audio: task.audio == null ? null : replace(task.audio!),
|
||||
choices: task.choices.map(replace).toList(),
|
||||
answerIndex: task.answerIndex,
|
||||
requiredWords: task.requiredWords.map(replace).toList(),
|
||||
abilityId: task.abilityId,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
List<AssessmentTask> _tasks(
|
||||
String version,
|
||||
List<String> listening,
|
||||
List<String> reading,
|
||||
List<String> writing,
|
||||
List<String> speaking,
|
||||
) {
|
||||
AssessmentTask choice(String raw, AssessmentSkill skill, int index) {
|
||||
final fields = raw.split('|');
|
||||
return AssessmentTask(
|
||||
id: 'A0-$version-${skill.name[0].toUpperCase()}${index + 1}',
|
||||
skill: skill,
|
||||
audio: skill == AssessmentSkill.listening ? fields[0] : null,
|
||||
prompt: skill == AssessmentSkill.listening ? '选择正确答案' : fields[0],
|
||||
choices: skill == AssessmentSkill.listening
|
||||
? fields.sublist(1, 4)
|
||||
: fields.sublist(1, 3),
|
||||
answerIndex: int.parse(fields.last),
|
||||
abilityId: 'A0-C${(index % 10 + 1).toString().padLeft(2, '0')}',
|
||||
);
|
||||
}
|
||||
|
||||
AssessmentTask open(String raw, AssessmentSkill skill, int index) {
|
||||
final fields = raw.split('|');
|
||||
return AssessmentTask(
|
||||
id: 'A0-$version-${skill.name[0].toUpperCase()}${index + 1}',
|
||||
skill: skill,
|
||||
prompt: fields[0],
|
||||
requiredWords: fields[1].split(','),
|
||||
abilityId: 'A0-C${(index % 10 + 1).toString().padLeft(2, '0')}',
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
...List.generate(
|
||||
listening.length,
|
||||
(i) => choice(listening[i], AssessmentSkill.listening, i),
|
||||
),
|
||||
...List.generate(
|
||||
reading.length,
|
||||
(i) => choice(reading[i], AssessmentSkill.reading, i),
|
||||
),
|
||||
...List.generate(
|
||||
writing.length,
|
||||
(i) => open(writing[i], AssessmentSkill.writing, i),
|
||||
),
|
||||
...List.generate(
|
||||
speaking.length,
|
||||
(i) => open(speaking[i], AssessmentSkill.speaking, i),
|
||||
),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user