فهرست مقاله
اگر تماسگیرنده «نوری» را بخواهد، تبدیل درست صوت به همین واژه هنوز مقصد یکتا نمیسازد. ممکن است چند شخص این نام را داشته باشند. در این درس، یک دفترچهٔ کاملاً ساختگی با دو دپارتمان و سه شخص میسازیم؛ نامزدها را نگه میداریم و پیش از آمادهکردن برچسب مسیر، تأیید صریح میخواهیم. هیچ دادهٔ کارمند واقعی، صوت، مدل ASR یا انتقال تلفنی در مثال وجود ندارد.
۱. هویت را از نام و نام مستعار جدا کنیم

هویت هر مدخل یک شناسهٔ صریح است؛ نام نمایشی میتواند برای خواننده توضیح بدهد، اما کلید یکتا نیست. P01 را «نگار نوری» در پشتیبانی، P02 را «نوید نوری» در فروش و P03 را «نگار ثابت» در پشتیبانی تعریف کردهایم. نامها و نسبتها صرفاً برای تمریناند. دپارتمانها نیز D01 و D02 هستند و شمارهٔ داخلی یا حساب کاربری ندارند.
برای هر مدخل، alias یا صورت ورودی را صریح مینویسیم. «نوری» برای دو شخص ثبت شده و «نگار» هم به دو شخص میرسد. تکرارِ همان alias برای همان شناسه نباید نامزد دومِ ساختگی تولید کند؛ ولی استفادهٔ دو هویت از یک alias نباید با بازنویسی مقدار قبلی در دیکشنری، پنهان شود. بنابراین نگاشت از یک کلید به مجموعهای از شناسههاست.
مستندات مجموعههای پایتون، یکتایی عضو و نبود ترتیب ذاتی را توضیح میدهد. برای خروجی قابل تکرار، شناسهها را مرتب میکنیم؛ قرارگرفتن P01 پیش از P02 به معنای احتمال، اهمیت یا confidence بیشتر نیست. شمارش نامزدها نیز امتیاز تشخیص گفتار محسوب نمیشود.
۲. نامزدهای شخص و دپارتمان را با زمینه محدود کنیم

کلید جستوجو شامل نوع مدخل و alias است. درخواست شخص با درخواست دپارتمان یکسان نیست؛ مثلاً «دفتر» در نوع دپارتمان به D01 و D02 میرسد. اگر نوع را حذف کنیم، ممکن است پاسخ مربوط به دو مسئولیت متفاوت با هم مخلوط شود. فراخواننده باید نوع مورد نظر را صریح بدهد؛ این تابع از جملهٔ آزاد نیت استخراج نمیکند.
در درخواست شخص، راهنمای دپارتمان میتواند مجموعه را محدود کند. «نوری» بدون راهنما دو نامزد دارد؛ با D01 فقط P01 و با D02 فقط P02 میماند. این تقاطع، انتخاب حدسیِ نخستین نام نیست؛ یک شرط تازه و مشخص روی همان فهرست است. راهنمای ناشناخته خطا میدهد، نه اینکه بیاعلان نادیده گرفته شود.
راهنما همیشه کافی نیست: «نگار» همراه D01 هنوز P01 و P03 را باقی میگذارد، چون هر دو در پشتیبانیاند. در این وضعیت باید اطلاعات بیشتری گرفت یا مسیر جایگزین مستقلی طراحی کرد؛ برنامه به حساب کوتاهکردن مکالمه، یکی را خودکار انتخاب نمیکند. وضعیت ambiguous و فهرست هر دو شناسه در خروجی باقی میمانند.
۳. حتی نامزد یکتا را پیش از تصمیم مسیر تأیید کنیم

سه وضعیت lookup داریم: not_found، ambiguous و needs_confirmation. در هر سه، route_label هنوز None است. نام کاملِ «نگار نوری» در دفترچه یک نامزد دارد، ولی طبق سیاستِ خودمان همچنان نیازمند تأیید است. این الزام را حکم عمومیِ همهٔ تلفنهای هوشمند یا استاندارد VoiceXML معرفی نمیکنیم.
در توصیهنامهٔ VoiceXML 2.0، مثال دفترچهٔ صوتی مرحلهٔ دریافت شخص و محل را از تأیید جدا نشان میدهد. ما فقط این تفکیک طراحی را به مسئلهٔ خودمان وصل میکنیم؛ مفسر VoiceXML، grammar و بازشناسی صوت اجرا نشدهاند. ساختار داده و قواعد این دفترچه، پیادهسازی همان مثال یا کل استاندارد نیستند.
تابع confirm دوباره lookup را روی دفترچهٔ دادهشده انجام میدهد و فقط برای نامزد یکتا، شناسهٔ مورد انتظارِ برابر و bool واقعی ادامه میدهد. True برچسب دادهای person:P01 را آماده میکند؛ False وضعیت not_confirmed و برچسب تهی میدهد. رشتهٔ «yes» یا عدد یک بهجای bool پذیرفته نیست؛ این تابع خودش «بله» را از صوت تشخیص نمیدهد.
۴. کد کامل و جدول دستی را با مرجع خطی مقایسه کنیم

فایل name_ambiguity.py با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. دفترچه حداکثر شانزده مدخل و هر مدخل یک تا شش alias دارد. نام و alias حداکثر چهل نقطهٔ کد پایتوناند؛ این کران، تعداد بایت یا نویسهٔ دیداری نیست. فاصلهٔ ابتدا و انتها و نویسههای کنترلیِ مشخصشده رد میشوند.
"""Synthetic offline alias candidates and explicit confirmation; no ASR or calls."""
import copy
import json
import platform
import re
from dataclasses import dataclass
from types import MappingProxyType
@dataclass(frozen=True)
class Entry:
entity_id: str
kind: str
name: str
department: str | None
aliases: frozenset
def text_ok(value):
return type(value) is str and 1 <= len(value) <= 40 and value == value.strip() and not any(ord(c) < 32 or ord(c) == 127 for c in value)
class Directory:
def __init__(self, rows):
if type(rows) is not list or not 1 <= len(rows) <= 16:
raise ValueError('records')
records, index = {}, {}
for row in rows:
if type(row) is not dict or set(row) != {'id', 'kind', 'name', 'department', 'aliases'}:
raise ValueError('record_schema')
entity_id, kind = row['id'], row['kind']
if type(kind) is not str or kind not in ('person', 'department'):
raise ValueError('record_identity')
prefix = 'P' if kind == 'person' else 'D'
if type(entity_id) is not str or re.fullmatch(prefix + r'[0-9]{2}', entity_id) is None or entity_id in records:
raise ValueError('record_identity')
if not text_ok(row['name']) or type(row['aliases']) is not list or not 1 <= len(row['aliases']) <= 6:
raise ValueError('record_text')
if not all(text_ok(alias) for alias in row['aliases']):
raise ValueError('record_text')
if kind == 'department' and row['department'] is not None:
raise ValueError('department_reference')
if kind == 'person' and (type(row['department']) is not str or re.fullmatch(r'D[0-9]{2}', row['department']) is None):
raise ValueError('department_reference')
entry = Entry(entity_id, kind, row['name'], row['department'], frozenset(row['aliases']))
records[entity_id] = entry
for alias in entry.aliases:
index.setdefault((kind, alias), set()).add(entity_id)
if any(e.kind == 'person' and (e.department not in records or records[e.department].kind != 'department') for e in records.values()):
raise ValueError('department_reference')
self.entries = tuple(records.values())
self.by_id = MappingProxyType(records)
self.alias_index = MappingProxyType({key: frozenset(ids) for key, ids in index.items()})
def resolve(directory, kind, alias, department_hint=None):
if type(kind) is not str or kind not in ('person', 'department'):
raise ValueError('kind')
if not text_ok(alias):
raise ValueError('alias')
if department_hint is not None:
if kind != 'person' or type(department_hint) is not str or department_hint not in directory.by_id or directory.by_id[department_hint].kind != 'department':
raise ValueError('department_hint')
candidates = directory.alias_index.get((kind, alias), frozenset())
if department_hint is not None:
candidates = candidates & frozenset(e.entity_id for e in directory.entries if e.department == department_hint)
ids = sorted(candidates)
status = 'not_found' if not ids else 'needs_confirmation' if len(ids) == 1 else 'ambiguous'
return {'kind': kind, 'alias': alias, 'department_hint': department_hint, 'status': status,
'candidates': [{'id': key, 'name': directory.by_id[key].name, 'department': directory.by_id[key].department} for key in ids],
'route_label': None}
def confirm(directory, kind, alias, expected_id, answer, department_hint=None):
if type(answer) is not bool:
raise ValueError('boolean_confirmation_required')
proposal = resolve(directory, kind, alias, department_hint)
if proposal['status'] != 'needs_confirmation':
raise ValueError('unique_candidate_required')
candidate = proposal['candidates'][0]
if type(expected_id) is not str or expected_id != candidate['id']:
raise ValueError('candidate_mismatch')
return {'status': 'route_label_prepared' if answer else 'not_confirmed',
'candidate_id': expected_id,
'route_label': kind + ':' + expected_id if answer else None}
def linear_ids(raw_rows, kind, alias, hint):
"""Independent raw-fixture scan, without Directory, alias_index or resolve."""
result = []
for row in raw_rows:
if row['kind'] == kind and alias in row['aliases']:
if hint is None or row['department'] == hint:
result.append(row['id'])
return sorted(result)
def synthetic_rows():
return [
{'id': 'D01', 'kind': 'department', 'name': 'پشتیبانی', 'department': None, 'aliases': ['پشتیبانی', 'فنی', 'دفتر']},
{'id': 'D02', 'kind': 'department', 'name': 'فروش', 'department': None, 'aliases': ['فروش', 'دفتر']},
{'id': 'P01', 'kind': 'person', 'name': 'نگار نوری', 'department': 'D01', 'aliases': ['نگار نوری', 'نوری', 'نگار', 'نوری']},
{'id': 'P02', 'kind': 'person', 'name': 'نوید نوری', 'department': 'D02', 'aliases': ['نوید نوری', 'نوری']},
{'id': 'P03', 'kind': 'person', 'name': 'نگار ثابت', 'department': 'D01', 'aliases': ['نگار ثابت', 'نگار']},
]
def rejects(call, message):
try:
call()
except ValueError as error:
return str(error) == message
return False
def run_example():
rows = synthetic_rows()
original = copy.deepcopy(rows)
directory = Directory(rows)
queries = [('person', 'نوری', None), ('person', 'نوری', 'D01'), ('person', 'نوری', 'D02'),
('person', 'نگار', 'D01'), ('department', 'دفتر', None), ('department', 'پشتیبانی', None),
('person', 'دکتر نوری', None), ('person', 'نگار نوری', None)]
proposals = [resolve(directory, kind, alias, hint) for kind, alias, hint in queries]
expected_statuses = ['ambiguous', 'needs_confirmation', 'needs_confirmation', 'ambiguous',
'ambiguous', 'needs_confirmation', 'not_found', 'needs_confirmation']
expected_ids = [['P01', 'P02'], ['P01'], ['P02'], ['P01', 'P03'], ['D01', 'D02'], ['D01'], [], ['P01']]
ids = lambda result: [candidate['id'] for candidate in result['candidates']]
positive = confirm(directory, 'person', 'نوری', 'P01', True, 'D01')
negative = confirm(directory, 'person', 'نوری', 'P01', False, 'D01')
cases, oracle_ok = 0, True
aliases = sorted({alias for row in original for alias in row['aliases']} | {'دکتر نوری', 'نگارنوری', 'نورى'})
for kind in ('person', 'department'):
for alias in aliases:
for hint in ((None, 'D01', 'D02') if kind == 'person' else (None,)):
oracle_ok &= ids(resolve(directory, kind, alias, hint)) == linear_ids(original, kind, alias, hint)
cases += 1
changed = resolve(directory, 'person', 'نوری')
changed['candidates'][0]['name'] = 'changed'
rows[2]['aliases'].append('سرپرست')
rows[2]['name'] = 'changed input'
checks = {
'hand_eight_statuses_and_candidate_identity_matrix': [p['status'] for p in proposals] == expected_statuses and [ids(p) for p in proposals] == expected_ids,
'shared_surname_keeps_both_people': ids(proposals[0]) == ['P01', 'P02'],
'department_hint_can_narrow_surname_to_distinct_people': ids(proposals[1]) == ['P01'] and ids(proposals[2]) == ['P02'],
'department_hint_is_not_always_sufficient': proposals[3]['status'] == 'ambiguous' and ids(proposals[3]) == ['P01', 'P03'],
'department_alias_has_its_own_ambiguity': proposals[4]['status'] == 'ambiguous' and ids(proposals[4]) == ['D01', 'D02'],
'unique_full_name_still_requires_confirmation': proposals[7]['status'] == 'needs_confirmation' and proposals[7]['route_label'] is None,
'explicit_positive_confirmation_only_prepares_label': positive == {'status': 'route_label_prepared', 'candidate_id': 'P01', 'route_label': 'person:P01'},
'explicit_negative_confirmation_prepares_no_route': negative == {'status': 'not_confirmed', 'candidate_id': 'P01', 'route_label': None},
'ambiguous_and_absent_candidates_cannot_be_confirmed': rejects(lambda: confirm(directory, 'person', 'نوری', 'P01', True), 'unique_candidate_required') and rejects(lambda: confirm(directory, 'person', 'دکتر نوری', 'P01', True), 'unique_candidate_required'),
'confirmation_must_name_the_exact_unique_candidate': rejects(lambda: confirm(directory, 'person', 'نوری', 'P02', True, 'D01'), 'candidate_mismatch'),
'confirmation_requires_actual_bool': all(rejects(lambda answer=answer: confirm(directory, 'person', 'نوری', 'P01', answer, 'D01'), 'boolean_confirmation_required') for answer in (1, 'yes', None)),
'no_fuzzy_letter_mapping_or_joined_name_guess': resolve(directory, 'person', 'نورى')['status'] == 'not_found' and resolve(directory, 'person', 'نگارنوری')['status'] == 'not_found',
'kind_and_department_hint_are_explicit_known_entities': rejects(lambda: resolve(directory, 'other', 'نوری'), 'kind') and rejects(lambda: resolve(directory, 'person', 'نوری', 'D99'), 'department_hint') and rejects(lambda: resolve(directory, 'department', 'دفتر', 'D01'), 'department_hint'),
'repeated_same_alias_does_not_duplicate_entity': len([c for c in proposals[0]['candidates'] if c['id'] == 'P01']) == 1 and directory.by_id['P01'].aliases == frozenset({'نگار نوری', 'نوری', 'نگار'}),
'record_shape_text_limits_and_alias_input_types': rejects(lambda: Directory([]), 'records') and rejects(lambda: Directory(synthetic_rows() * 4), 'records') and rejects(lambda: Directory([{**original[0], 'extra': 1}]), 'record_schema') and rejects(lambda: resolve(directory, 'person', ' نوری'), 'alias') and rejects(lambda: resolve(directory, 'person', 'x' * 41), 'alias') and rejects(lambda: resolve(directory, 'person', None), 'alias'),
'ids_unique_and_department_references_validated': rejects(lambda: Directory(original + [original[0]]), 'record_identity') and rejects(lambda: Directory([{**r, 'department': 'D99'} if r['id'] == 'P01' else r for r in original]), 'department_reference'),
'independent_linear_reference_over_all_authored_aliases': cases == len(aliases) * 4 and bool(oracle_ok),
'compiled_snapshot_and_fresh_results_ignore_caller_mutations': resolve(directory, 'person', 'سرپرست')['status'] == 'not_found' and resolve(directory, 'person', 'نگار نوری')['candidates'][0]['name'] == 'نگار نوری' and resolve(directory, 'person', 'نوری') == proposals[0],
}
assert len(checks) == 18 and all(type(value) is bool and value for value in checks.values()), checks
return {'python': platform.python_version(), 'dependencies': 'Python standard library only',
'scope': 'Entirely synthetic exact-alias directory after textual input; no ASR, fuzzy normalization, real directory, calls, authentication or exactly-once guarantee',
'results': {'synthetic_directory': original, 'eight_hand_proposals': proposals,
'hand_expected_candidate_ids': expected_ids, 'positive_confirmation': positive,
'negative_confirmation': negative, 'independent_linear_cases_inside_one_group': cases,
'limits': {'max_entries': 16, 'max_aliases_per_entry': 6, 'max_text_python_codepoints': 40,
'ordering': 'sorted entity IDs only, not confidence or importance',
'calls_made': 0, 'speech_models_executed': 0}}, 'checks': checks}
if __name__ == '__main__':
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
جدول هشت درخواست، سه پاسخ مبهم، چهار پاسخ نیازمند تأیید و یک not_found دارد. «نوری»، «نگار در پشتیبانی» و دپارتمان «دفتر» سه ابهاماند. «دکتر نوری» ثبت نشده و بدون حذف خودکار عنوان، پیدا نمیشود. شکل چسبیدهٔ «نگارنوری» یا حرف متفاوت در «نورى» هم خودکار به alias شناختهشده تبدیل نمیشوند؛ این درس نرمالساز یا تطبیق فازی نیست.
هجده گروه اجرایی داریم. مرجع مستقل، رکوردهای خامِ fixture را خطی میگردد و از سازندهٔ Directory، نمایهٔ alias یا resolve استفاده نمیکند. دوازده صورت ورودی با حالتهای معتبر نوع و راهنمای دپارتمان، چهلوهشت مقایسه در یک گروه مرجع میسازند. پاسخ دستیِ هشتتایی نیز جداست؛ اجرای دوباره یا زیرحالتها گروه تازه شمرده نمیشوند.
۵. مرز تأیید و چهار پاسخ را روشن نگه داریم

دادهٔ داخلی از ورودی ساخته میشود؛ تغییر بعدی فهرست alias یا نام در ورودیِ آزمایش، نتیجهٔ دفترچهٔ ساختهشده را عوض نمیکند. MappingProxyType بهتنهایی snapshot نیست؛ نمای پویاست. جداسازی از ورودی در اینجا حاصل ساختن نگاشتها و مقدارهای تازه است، نه تضمین امنیتی یا تغییرناپذیریِ کاملِ شیء Directory.
{
"python": "3.14.5",
"dependencies": "Python standard library only",
"scope": "Entirely synthetic exact-alias directory after textual input; no ASR, fuzzy normalization, real directory, calls, authentication or exactly-once guarantee",
"results": {
"synthetic_directory": [
{
"id": "D01",
"kind": "department",
"name": "پشتیبانی",
"department": null,
"aliases": [
"پشتیبانی",
"فنی",
"دفتر"
]
},
{
"id": "D02",
"kind": "department",
"name": "فروش",
"department": null,
"aliases": [
"فروش",
"دفتر"
]
},
{
"id": "P01",
"kind": "person",
"name": "نگار نوری",
"department": "D01",
"aliases": [
"نگار نوری",
"نوری",
"نگار",
"نوری"
]
},
{
"id": "P02",
"kind": "person",
"name": "نوید نوری",
"department": "D02",
"aliases": [
"نوید نوری",
"نوری"
]
},
{
"id": "P03",
"kind": "person",
"name": "نگار ثابت",
"department": "D01",
"aliases": [
"نگار ثابت",
"نگار"
]
}
],
"eight_hand_proposals": [
{
"kind": "person",
"alias": "نوری",
"department_hint": null,
"status": "ambiguous",
"candidates": [
{
"id": "P01",
"name": "نگار نوری",
"department": "D01"
},
{
"id": "P02",
"name": "نوید نوری",
"department": "D02"
}
],
"route_label": null
},
{
"kind": "person",
"alias": "نوری",
"department_hint": "D01",
"status": "needs_confirmation",
"candidates": [
{
"id": "P01",
"name": "نگار نوری",
"department": "D01"
}
],
"route_label": null
},
{
"kind": "person",
"alias": "نوری",
"department_hint": "D02",
"status": "needs_confirmation",
"candidates": [
{
"id": "P02",
"name": "نوید نوری",
"department": "D02"
}
],
"route_label": null
},
{
"kind": "person",
"alias": "نگار",
"department_hint": "D01",
"status": "ambiguous",
"candidates": [
{
"id": "P01",
"name": "نگار نوری",
"department": "D01"
},
{
"id": "P03",
"name": "نگار ثابت",
"department": "D01"
}
],
"route_label": null
},
{
"kind": "department",
"alias": "دفتر",
"department_hint": null,
"status": "ambiguous",
"candidates": [
{
"id": "D01",
"name": "پشتیبانی",
"department": null
},
{
"id": "D02",
"name": "فروش",
"department": null
}
],
"route_label": null
},
{
"kind": "department",
"alias": "پشتیبانی",
"department_hint": null,
"status": "needs_confirmation",
"candidates": [
{
"id": "D01",
"name": "پشتیبانی",
"department": null
}
],
"route_label": null
},
{
"kind": "person",
"alias": "دکتر نوری",
"department_hint": null,
"status": "not_found",
"candidates": [],
"route_label": null
},
{
"kind": "person",
"alias": "نگار نوری",
"department_hint": null,
"status": "needs_confirmation",
"candidates": [
{
"id": "P01",
"name": "نگار نوری",
"department": "D01"
}
],
"route_label": null
}
],
"hand_expected_candidate_ids": [
[
"P01",
"P02"
],
[
"P01"
],
[
"P02"
],
[
"P01",
"P03"
],
[
"D01",
"D02"
],
[
"D01"
],
[],
[
"P01"
]
],
"positive_confirmation": {
"status": "route_label_prepared",
"candidate_id": "P01",
"route_label": "person:P01"
},
"negative_confirmation": {
"status": "not_confirmed",
"candidate_id": "P01",
"route_label": null
},
"independent_linear_cases_inside_one_group": 48,
"limits": {
"max_entries": 16,
"max_aliases_per_entry": 6,
"max_text_python_codepoints": 40,
"ordering": "sorted entity IDs only, not confidence or importance",
"calls_made": 0,
"speech_models_executed": 0
}
},
"checks": {
"hand_eight_statuses_and_candidate_identity_matrix": true,
"shared_surname_keeps_both_people": true,
"department_hint_can_narrow_surname_to_distinct_people": true,
"department_hint_is_not_always_sufficient": true,
"department_alias_has_its_own_ambiguity": true,
"unique_full_name_still_requires_confirmation": true,
"explicit_positive_confirmation_only_prepares_label": true,
"explicit_negative_confirmation_prepares_no_route": true,
"ambiguous_and_absent_candidates_cannot_be_confirmed": true,
"confirmation_must_name_the_exact_unique_candidate": true,
"confirmation_requires_actual_bool": true,
"no_fuzzy_letter_mapping_or_joined_name_guess": true,
"kind_and_department_hint_are_explicit_known_entities": true,
"repeated_same_alias_does_not_duplicate_entity": true,
"record_shape_text_limits_and_alias_input_types": true,
"ids_unique_and_department_references_validated": true,
"independent_linear_reference_over_all_authored_aliases": true,
"compiled_snapshot_and_fresh_results_ignore_caller_mutations": true
}
}
این مرحله پس از دریافت متن قرار دارد و جای ارزیابی تشخیص گفتار فارسی را نمیگیرد. برچسب مسیر هم هنوز اجرای مسیریابی تلفن گویا نیست. فراخوانی دوبارهٔ تأیید مثبت میتواند همان برچسب را دوباره بسازد؛ اجرای یکبارهٔ اقدام بیرونی، احراز هویت و مجوز دسترسی در این مثال حل نشدهاند.
پرسش اول: برای «نوری» بدون راهنما، P01 را چون اول فهرست است انتخاب کنیم؟ پاسخ: خیر؛ دو نامزد داریم و ترتیب شناسه، رتبهٔ اطمینان نیست. تأیید روی این ابهام خطا میدهد.
پرسش دوم: آیا «نگار در پشتیبانی» یکتا میشود؟ پاسخ: خیر؛ P01 و P03 هر دو شرط را دارند. گرفتن دپارتمان در این مورد برای رفع ابهام کافی نیست.
پرسش سوم: نام کامل یکتا یعنی تماس برقرار شده است؟ پاسخ: خیر؛ ابتدا نیازمند تأیید است و پس از True نیز فقط رشتهٔ برچسب ساخته میشود. هیچ تماس واقعی نداریم.
پرسش چهارم: آیا ۴۸ مقایسه، دقت مدل صوتی است؟ پاسخ: خیر؛ فقط بررسی قرارداد روی دادهٔ تألیفی است. منابع در ۲۵ مهر ۱۴۰۵ خوانده شدند؛ توصیهنامه مربوط به مارس ۲۰۰۴ است و مستندات 3.14.8 از اجرای 3.14.5 جدا ثبت میشوند. این آموزش پایه، خبر تازه یا نتیجهٔ سامانهٔ واقعی نیست.
منابع و مطالعهٔ بیشتر
- W3C؛ VoiceXML2.0، دفترچهٔ صوتی و مرحلهٔ تأیید
- Python؛ یکتایی، عضویت و تقاطع مجموعهها
- Python؛ نمای MappingProxyType و تفاوت آن با snapshot
بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

