Lesson content complete: examples/steps for all 3,778 lessons, 1,775 lessons with fixed figures, 38 quiz fixes
- curriculum.json: example/steps for every real lesson, figures (2,224) hand-written and visually checked - questions.json: 3,703 questions (24 added, 14 keys fixed) - gen_lesson_figures: strict check, thin-figure filter, recheck, --max-usd; API figures limited to plane/fraction/numberline/solid Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,28 @@
|
||||
"""
|
||||
شکلِ ثابتِ درسها را یک بار میسازد و در `Lesson.figures` مینویسد.
|
||||
|
||||
همان مسیرِ ارزانِ دستیار: همان مدلِ چت، همان کاتالوگِ <diagram>، همان
|
||||
اعتبارسنجِ `rag.services.diagram.validate`. فرقش این است که نتیجه ذخیره میشود و
|
||||
بعد از تأییدِ چشمی با `seed_content --export` به curriculum.json میرود.
|
||||
فقط درسهایی که شکل لازم دارند شکل میگیرند؛ مدل میتواند `[]` بدهد.
|
||||
آزموده نشده: درستیِ هندسی/ریاضیِ شکل؛ باید با چشم دید.
|
||||
همان مسیرِ ارزانِ دستیار: همان مدلِ چت، همان کاتالوگِ <diagram>، همان اعتبارسنجِ
|
||||
`rag.services.diagram.validate`. فرقش این است که نتیجه ذخیره میشود و بعد از تأییدِ چشمی با
|
||||
`seed_content --export` به curriculum.json میرود.
|
||||
|
||||
python manage.py gen_lesson_figures --grade 12 --limit 4 [--redo] [--dry-run]
|
||||
سه چیز که هزینه و خطا را پایین میآورد:
|
||||
1. **پیشصافیِ رایگان:** اگر متنِ درس به هیچ بخشِ کاتالوگ نمیخورد (حسابِ عددیِ ساده)، مدل صدا
|
||||
زده نمیشود و درس «بینیازِ شکل» ثبت میشود.
|
||||
2. **فقط بخشهای مربوطِ کاتالوگ** (نه کلِ آن، ~۳٬۵۰۰ توکن) کنارِ درس میرود.
|
||||
3. **گامِ وارسیِ مستقل:** هر شکلی که مدل ساخت، مدلِ دوم با متنِ درس مقایسه میکند؛ شکلی که
|
||||
همان چیزِ متن را نشان نمیدهد (عدد/برچسبِ ناهمخوان، قدمِ اشتباه) ذخیره نمیشود.
|
||||
|
||||
سقفِ هزینه با فرمولِ رسمیِ gpt-4.1-mini محاسبه میشود (۰٫۴۰ و ۱٫۶۰ دلار در هر میلیون توکن). صورتحسابِ
|
||||
واقعیِ GapGPT در سنجشهای قبلی چند برابر کمتر بود، پس سقف محافظهکارانه است. آزموده نشده: درستیِ
|
||||
هندسی/ریاضیِ شکل؛ وارسیِ مدلِ دوم جایگزینِ دیدنِ چشمی نیست.
|
||||
|
||||
python manage.py gen_lesson_figures --grade 12 --limit 20 --max-usd 2 --ledger fig_ledger.json
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
@@ -18,85 +30,259 @@ from content.models import Lesson
|
||||
from rag.services import clients, diagram as diagram_mod
|
||||
from rag.services.generation import chat_model_for, completion_kwargs
|
||||
|
||||
USD_PER_M_IN = 0.40
|
||||
USD_PER_M_OUT = 1.60
|
||||
MAX_FIGURES = 3
|
||||
|
||||
PROMPT = (
|
||||
'تو برای یک درسِ ریاضی (اپِ آموزشی) شکلهای آموزشی میسازی. متنِ درس: «توضیح»، «مثال» و '
|
||||
'«قدمها» (شمارهی قدمها از ۱، همان شمارهای که کاربر میبیند). فقط جاهایی شکل بگذار که دیدنش مفهوم را روشن میکند (نمودار، '
|
||||
'انتقال، تقارن، محور، جدول، هندسه)؛ هر شکل باید دقیقاً همان چیزی را نشان دهد که آن بخش '
|
||||
'میگوید، با همان اعداد. بخشِ بینیاز شکل نمیگیرد؛ اگر هیچجا لازم نبود []. حداکثر ۵ شکل.\n'
|
||||
'خروجی فقط یک آرایهی JSON: [{"where":"intro"|"example"|"step","index":0,"diagram":{...}}] '
|
||||
'(index فقط برای step و همان شمارهی ۱بهبعدِ قدم). بدونِ توضیح و بدونِ ```.\n'
|
||||
'مثالهای کاتالوگ فقط قالباند: نقطه، تابع یا آیتمی را که این درس لازم ندارد کپی نکن؛ فقط '
|
||||
'چیزهایی بگذار که همین شکل نشان میدهد (برای نمودارِ تابع معمولاً فقط items با type=function). نمودارِ خطشکسته یا تکهای از نقطهها را '
|
||||
'با type=segment بین نقطههای متوالی بکش، نه type=line (آن خطِ بیپایان است). نقطههایی که '
|
||||
'در items نمیآیند تعریف نکن. شکلِ هر قدم باید همان چیزی را نشان دهد که متنِ همان قدم میگوید '
|
||||
'(نه قدمِ قبل یا بعد). بازهی x نمودارِ تابع را کوچک بگیر (مثلاً دو واحد دور از رأس/نقطههای مهم) تا '
|
||||
'مقدارِ y از حدود ۹ بیشتر نشود و منحنی له نشود؛ marks فقط برای یک منحنی بگذار تا برچسبها روی هم نیفتند.\n\n'
|
||||
'تو برای یک درسِ ریاضی (اپِ آموزشی) شکلهای آموزشی میسازی. متنِ درس: «توضیح»، «مثال» و «قدمها» '
|
||||
'(شمارهی قدمها از ۱، همان شمارهای که کاربر میبیند).\n'
|
||||
'قاعدهها:\n'
|
||||
f'- فقط جایی شکل بگذار که دیدنش مفهوم یا مسئله را روشن میکند؛ حداکثر {MAX_FIGURES} شکل. '
|
||||
'شکلِ تزیینی یا تکراری نساز. اگر هیچجا لازم نبود []. ترجیح با «example» و «step» است؛ '
|
||||
'«intro» فقط وقتی متنِ توضیح خودش عدد یا نمادِ مشخص میدهد.\n'
|
||||
'- هر شکل باید دقیقاً همان را نشان دهد که آن بخش میگوید، با همان اعداد و نامها؛ چیزی که در '
|
||||
'متن نیست اختراع نکن. برای بخشی که در متن نیست (مثال/قدم) شکل نساز.\n'
|
||||
'- نمودارِ خطشکسته یا تکهای را با type=segment بین نقطههای متوالی بکش، نه type=line. '
|
||||
'برای نمودارِ تابع بازهی x را کوچک بگیر (مقدارِ y از حدود ۹ بیشتر نشود) و فقط items لازم را بگذار؛ '
|
||||
'marks فقط برای یک منحنی. نقطههایی که در items نمیآیند تعریف نکن.\n'
|
||||
'- شکلِ فقطنقطه (چند نقطهی بیارتباط) نساز؛ نقطه را روی نمودار یا شکلِ مربوطش بگذار. ضابطهی تابع را '
|
||||
'عیناً از متن بردار (ضریب و انتقال را حذف نکن). برای مثلثِ متساویالاضلاع/متساویالساقین و مانندش '
|
||||
'طولها را با نوعش سازگار بده. نوعِ pattern/groups را فقط وقتی بگذار که خودِ متن شکل یا شمارشِ مشخصی میدهد.\n'
|
||||
'- شکلِ هر قدم همان را نشان دهد که متنِ همان قدم میگوید (نه قدمِ قبل یا بعد).\n'
|
||||
'- مثالهای کاتالوگ فقط قالباند؛ نقطه، تابع یا آیتمی را که این درس لازم ندارد کپی نکن.\n'
|
||||
'خروجی فقط یک آرایهی JSON: [{"where":"intro"|"example"|"step","index":1,"diagram":{...}}] '
|
||||
'(index فقط برای step و همان شمارهی ۱بهبعدِ قدم). بدونِ توضیح و بدونِ ```.\n\n'
|
||||
)
|
||||
|
||||
CHECK = (
|
||||
'بازبینِ سختگیرِ ریاضی هستی. یک درس و شکلهایی که برایش ساختهاند (هر شکل یک JSON) داده میشود. '
|
||||
'برای هر شکل ابتدا در یک جمله بنویس «shows»: دقیقاً چه چیزی میکشد (ضابطهی هر تابع، مختصات نقطهها، '
|
||||
'طول ضلعها و زاویهها، بازهها، عددهای جدول، موقعیت و برچسبها). بعد با متنِ همان بخشِ درس مقایسه کن. '
|
||||
'ok=false بده اگر: ضابطه یا عددی با متن نمیخواند (مثلاً ضریب یا انتقال گم شده)؛ شکل از نظر ریاضی/هندسی '
|
||||
'ناسازگار است (مثلاً مثلثِ متساویالاضلاع با ضلعهای نابرابر)؛ موقعیتِ نقطهها با برچسبشان نمیخواند؛ '
|
||||
'شکل مفهومِ اصلیِ آن بخش را نشان نمیدهد یا فقط نقطههای پراکنده/برچسبِ بیمعنی است؛ چیزی اختراع شده؛ '
|
||||
'شکلِ قدم به همان قدم مربوط نیست. هر شکِ جدی → ok=false. سلیقهای ایراد نگیر. '
|
||||
'خروجی فقط JSON: {"verdicts":[{"i":0,"shows":"...","ok":true|false,"why":"یک جمله"}]} به ترتیبِ شکلها.'
|
||||
)
|
||||
|
||||
|
||||
def _lesson_text(lesson: Lesson) -> str:
|
||||
steps = '\n'.join(f'قدم {i}: {s}' for i, s in enumerate(lesson.steps or [], 1))
|
||||
return f'عنوان: {lesson.title}\nتوضیح: {lesson.intro_text}\nمثال: {lesson.example}\n{steps}'
|
||||
|
||||
|
||||
def _spec_for(text: str) -> str:
|
||||
"""فقط بخشهایی از کاتالوگ که کلمههایشان در متن هست؛ بیبخش → '' (مدل صدا زده نمیشود)."""
|
||||
parts = [section for pattern, section in diagram_mod.SECTIONS if pattern.search(text)]
|
||||
return '\n'.join([diagram_mod._HEAD, *parts]) if parts else ''
|
||||
|
||||
|
||||
_SUBSTANTIVE = {'function', 'segment', 'line', 'ray', 'polygon', 'triangle', 'quad', 'regularPolygon', 'circle',
|
||||
'angle', 'exteriorAngle', 'arc', 'vector'}
|
||||
|
||||
|
||||
def _is_thin(diagram: dict) -> bool:
|
||||
"""
|
||||
شکلِ صفحهای که هیچ منحنی، پارهخط یا شکلی ندارد و فقط نقطهی پراکنده است چیزی نشان نمیدهد
|
||||
(مثلاً چند نقطهی A و B و C بیارتباط). نوعِ تازهای که این فهرست نمیشناسد «محتوادار» حساب میشود.
|
||||
"""
|
||||
if diagram.get('kind') != 'plane':
|
||||
return False
|
||||
items = diagram.get('items') or []
|
||||
if any(i.get('type') in _SUBSTANTIVE or i.get('type') not in ('point', 'label', None) for i in items):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _json_list(raw: str):
|
||||
raw = re.sub(r'^```(?:json)?|```$', '', (raw or '').strip()).strip()
|
||||
start = raw.index('[')
|
||||
items, _ = json.JSONDecoder().raw_decode(raw[start:])
|
||||
if not isinstance(items, list):
|
||||
raise ValueError('not a list')
|
||||
return items
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'ساختِ شکلِ ثابتِ درسها (یک بار، ذخیره در Lesson.figures).'
|
||||
|
||||
def add_arguments(self, p):
|
||||
p.add_argument('--grade', type=int, required=True)
|
||||
p.add_argument('--grade', type=int, help='بدونِ آن همهی پایهها')
|
||||
p.add_argument('--limit', type=int, default=4)
|
||||
p.add_argument('--lesson', type=int, help='فقط همین شناسه')
|
||||
p.add_argument('--redo', action='store_true', help='درسهایی که شکل دارند را هم بساز')
|
||||
p.add_argument('--redo', action='store_true', help='درسهایی که شکل دارند هم')
|
||||
p.add_argument('--dry-run', action='store_true')
|
||||
p.add_argument('--workers', type=int, default=4)
|
||||
p.add_argument('--max-usd', type=float, default=2.0, help='سقفِ هزینه (فرمولِ رسمی)؛ رسید، میایستد')
|
||||
p.add_argument('--ledger', help='JSON از درسهای بررسیشده ({id: نتیجه}) تا دوباره پول نگیرند')
|
||||
p.add_argument('--quiet', action='store_true')
|
||||
p.add_argument('--recheck', action='store_true',
|
||||
help='شکلهای ذخیرهشده را دوباره با وارسیِ سختگیر بسنج و ناموفقها را پاک کن (بیساخت)')
|
||||
p.add_argument('--no-check', action='store_true', help='گامِ وارسیِ مستقل را رد کن (فقط برای آزمایش)')
|
||||
|
||||
def handle(self, *a, **o):
|
||||
qs = Lesson.objects.filter(node__unit__grade__number=o['grade']).order_by(
|
||||
'node__unit__order', 'node__order', 'id')
|
||||
if o['recheck']:
|
||||
return self._recheck(o)
|
||||
qs = Lesson.objects.exclude(example='').order_by(
|
||||
'node__unit__grade__number', 'node__unit__order', 'node__order', 'id')
|
||||
if o['grade']:
|
||||
qs = qs.filter(node__unit__grade__number=o['grade'])
|
||||
if o['lesson']:
|
||||
qs = qs.filter(pk=o['lesson'])
|
||||
if not o['redo']:
|
||||
qs = qs.filter(figures=[])
|
||||
ledger_path = Path(o['ledger']) if o['ledger'] else None
|
||||
ledger = json.loads(ledger_path.read_text()) if ledger_path and ledger_path.exists() else {}
|
||||
if not o['redo']:
|
||||
qs = qs.exclude(pk__in=[int(k) for k in ledger])
|
||||
lessons = list(qs[:o['limit']])
|
||||
if not lessons:
|
||||
raise CommandError('درسی پیدا نشد.')
|
||||
|
||||
model = chat_model_for('')
|
||||
client = clients.get_openai_client()
|
||||
spent = prompt_t = out_t = 0
|
||||
for lesson in lessons:
|
||||
body = (f'عنوان: {lesson.title}\nتوضیح: {lesson.intro_text}\nمثال: {lesson.example}\n'
|
||||
+ '\n'.join(f'قدم {i}: {s}' for i, s in enumerate(lesson.steps or [], 1)))
|
||||
lock = threading.Lock()
|
||||
usage = {'in': 0, 'out': 0}
|
||||
stats = {'no_trigger': 0, 'none': 0, 'saved': 0, 'figures': 0, 'rejected': 0, 'invalid': 0, 'stopped': 0}
|
||||
|
||||
def spent():
|
||||
return usage['in'] / 1e6 * USD_PER_M_IN + usage['out'] / 1e6 * USD_PER_M_OUT
|
||||
|
||||
def ask(system, user, **extra):
|
||||
r = client.chat.completions.create(
|
||||
model=model, **completion_kwargs(model),
|
||||
messages=[{'role': 'system', 'content': PROMPT + diagram_mod.DIAGRAM_SPEC},
|
||||
{'role': 'user', 'content': body}])
|
||||
model=model, **completion_kwargs(model), **extra,
|
||||
messages=[{'role': 'system', 'content': system}, {'role': 'user', 'content': user}])
|
||||
u = getattr(r, 'usage', None)
|
||||
spent += (getattr(u, 'total_tokens', 0) or 0)
|
||||
prompt_t += (getattr(u, 'prompt_tokens', 0) or 0)
|
||||
out_t += (getattr(u, 'completion_tokens', 0) or 0)
|
||||
raw = re.sub(r'^```(?:json)?|```$', '', (r.choices[0].message.content or '').strip()).strip()
|
||||
with lock:
|
||||
usage['in'] += getattr(u, 'prompt_tokens', 0) or 0
|
||||
usage['out'] += getattr(u, 'completion_tokens', 0) or 0
|
||||
return r.choices[0].message.content or ''
|
||||
|
||||
def record(lesson, outcome):
|
||||
with lock:
|
||||
if ledger_path and not o['dry_run']:
|
||||
ledger[str(lesson.id)] = outcome
|
||||
ledger_path.write_text(json.dumps(ledger, ensure_ascii=False))
|
||||
|
||||
def work(lesson):
|
||||
text = _lesson_text(lesson)
|
||||
spec = _spec_for(text)
|
||||
if not spec:
|
||||
with lock:
|
||||
stats['no_trigger'] += 1
|
||||
record(lesson, 'no_trigger')
|
||||
return
|
||||
if spent() >= o['max_usd']:
|
||||
with lock:
|
||||
stats['stopped'] += 1
|
||||
return
|
||||
try:
|
||||
start = raw.index('[')
|
||||
items, _ = json.JSONDecoder().raw_decode(raw[start:])
|
||||
if not isinstance(items, list):
|
||||
raise ValueError('not a list')
|
||||
except ValueError as e:
|
||||
items = _json_list(ask(PROMPT + spec, text))
|
||||
except (ValueError, KeyError) as e:
|
||||
with lock:
|
||||
stats['invalid'] += 1
|
||||
self.stderr.write(f'درس {lesson.id}: خروجیِ نامعتبر ({e}); ذخیره نشد.')
|
||||
continue
|
||||
return
|
||||
figures, dropped = [], 0
|
||||
for it in items:
|
||||
for it in items[:MAX_FIGURES + 2]:
|
||||
where = it.get('where') if isinstance(it, dict) else None
|
||||
spec = diagram_mod.validate(it.get('diagram')) if where else None
|
||||
s = diagram_mod.validate(it.get('diagram')) if where else None
|
||||
idx = it.get('index')
|
||||
if where == 'step' and isinstance(idx, int):
|
||||
idx -= 1 # مدل ۱بهبعد مینویسد؛ ذخیره از صفر
|
||||
if (spec is None or where not in ('intro', 'example', 'step')
|
||||
if (s is None or where not in ('intro', 'example', 'step')
|
||||
or (where == 'step' and not (isinstance(idx, int) and 0 <= idx < len(lesson.steps or [])))
|
||||
or (where == 'example' and not lesson.example)):
|
||||
dropped += 1
|
||||
continue
|
||||
for item in spec.get('items', []):
|
||||
if _is_thin(s):
|
||||
dropped += 1
|
||||
continue
|
||||
for item in s.get('items', []):
|
||||
if item.get('type') == 'segment':
|
||||
item.pop('ticks', None) # علامتِ «ضلع مساوی» روی نمودارِ تابع فقط شلوغی است
|
||||
figures.append({'where': where, **({'index': idx} if where == 'step' else {}), 'diagram': spec})
|
||||
self.stdout.write(f'درس {lesson.id} «{lesson.title}»: {len(figures)} شکل، {dropped} ردشده')
|
||||
for f in figures:
|
||||
self.stdout.write(' ' + json.dumps(f, ensure_ascii=False))
|
||||
if figures and not o['dry_run']:
|
||||
lesson.figures = figures
|
||||
figures.append({'where': where, **({'index': idx} if where == 'step' else {}), 'diagram': s})
|
||||
figures = figures[:MAX_FIGURES]
|
||||
if not figures:
|
||||
with lock:
|
||||
stats['none'] += 1
|
||||
record(lesson, 'none' if not items else 'invalid_all')
|
||||
return
|
||||
kept, notes = figures, []
|
||||
if not o['no_check']:
|
||||
try:
|
||||
verdicts = json.loads(ask(CHECK, text + '\n\nشکلها:\n' + '\n'.join(
|
||||
f'{i}: ' + json.dumps(f, ensure_ascii=False) for i, f in enumerate(figures)),
|
||||
response_format={'type': 'json_object'})).get('verdicts', [])
|
||||
ok = {v.get('i'): v for v in verdicts if isinstance(v, dict)}
|
||||
kept = [f for i, f in enumerate(figures) if ok.get(i, {}).get('ok') is True]
|
||||
notes = [f"{i}: {v.get('why', '')}" for i, v in ok.items() if v.get('ok') is not True]
|
||||
except (ValueError, KeyError, AttributeError) as e:
|
||||
kept, notes = [], [f'وارسی ناموفق: {e}']
|
||||
with lock:
|
||||
stats['rejected'] += len(figures) - len(kept)
|
||||
stats['figures'] += len(kept)
|
||||
if kept:
|
||||
stats['saved'] += 1
|
||||
self.stdout.write(f'درس {lesson.id} «{lesson.title}»: {len(kept)} شکل از {len(figures)} '
|
||||
f'({dropped} ردشده در اعتبارسنج)' + (f' | رد: {"; ".join(notes)}' if notes else ''))
|
||||
if not o['quiet']:
|
||||
for f in kept:
|
||||
self.stdout.write(' ' + json.dumps(f, ensure_ascii=False))
|
||||
if kept and not o['dry_run']:
|
||||
lesson.figures = kept
|
||||
lesson.save(update_fields=['figures'])
|
||||
self.stdout.write(f'توکنِ مصرفی: {spent} (ورودی {prompt_t}، خروجی {out_t})')
|
||||
record(lesson, f'figures:{len(kept)}' if kept else 'rejected')
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max(1, o['workers'])) as ex:
|
||||
list(ex.map(work, lessons))
|
||||
self.stdout.write(f'{stats}\nتوکن: ورودی {usage["in"]}، خروجی {usage["out"]}؛ هزینهی فرمولی ≈ ${spent():.3f}')
|
||||
|
||||
def _recheck(self, o):
|
||||
qs = Lesson.objects.exclude(figures=[]).order_by('node__unit__grade__number', 'id')
|
||||
if o['grade']:
|
||||
qs = qs.filter(node__unit__grade__number=o['grade'])
|
||||
lessons = list(qs[:o['limit']])
|
||||
model = chat_model_for('')
|
||||
client = clients.get_openai_client()
|
||||
lock = threading.Lock()
|
||||
usage = {'in': 0, 'out': 0}
|
||||
stats = {'lessons': 0, 'kept': 0, 'dropped': 0, 'errors': 0}
|
||||
|
||||
def work(lesson):
|
||||
figures = [f for f in lesson.figures if not _is_thin(f['diagram'])]
|
||||
thin = len(lesson.figures) - len(figures)
|
||||
text = _lesson_text(lesson)
|
||||
try:
|
||||
r = client.chat.completions.create(
|
||||
model=model, **completion_kwargs(model), response_format={'type': 'json_object'},
|
||||
messages=[{'role': 'system', 'content': CHECK}, {'role': 'user', 'content': text + '\n\nشکلها:\n' + '\n'.join(
|
||||
f'{i}: ' + json.dumps(f, ensure_ascii=False) for i, f in enumerate(figures))}])
|
||||
u = getattr(r, 'usage', None)
|
||||
ok = {v.get('i'): v for v in json.loads(r.choices[0].message.content or '{}').get('verdicts', [])
|
||||
if isinstance(v, dict)} if figures else {}
|
||||
except (ValueError, KeyError, AttributeError) as e:
|
||||
with lock:
|
||||
stats['errors'] += 1
|
||||
self.stderr.write(f'درس {lesson.id}: وارسی ناموفق ({e}); دست نخورد.')
|
||||
return
|
||||
kept = [f for i, f in enumerate(figures) if ok.get(i, {}).get('ok') is True]
|
||||
with lock:
|
||||
usage['in'] += getattr(u, 'prompt_tokens', 0) or 0
|
||||
usage['out'] += getattr(u, 'completion_tokens', 0) or 0
|
||||
stats['lessons'] += 1
|
||||
stats['kept'] += len(kept)
|
||||
stats['dropped'] += len(lesson.figures) - len(kept)
|
||||
why = '; '.join(f"{i}: {v.get('why', '')}" for i, v in ok.items() if v.get('ok') is not True)
|
||||
if len(kept) != len(lesson.figures) and not o['quiet']:
|
||||
self.stdout.write(f'درس {lesson.id} «{lesson.title}»: {len(kept)}/{len(lesson.figures)}'
|
||||
f' (بیمحتوا {thin}) | {why}')
|
||||
if len(kept) != len(lesson.figures) and not o['dry_run']:
|
||||
lesson.figures = kept
|
||||
lesson.save(update_fields=['figures'])
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max(1, o['workers'])) as ex:
|
||||
list(ex.map(work, lessons))
|
||||
cost = usage['in'] / 1e6 * USD_PER_M_IN + usage['out'] / 1e6 * USD_PER_M_OUT
|
||||
self.stdout.write(f'{stats}\nتوکن: ورودی {usage["in"]}، خروجی {usage["out"]}؛ هزینهی فرمولی ≈ ${cost:.3f}')
|
||||
|
||||
Reference in New Issue
Block a user