Презентация и учебные материалы: Excel и ИИ-агенты
This commit is contained in:
1 parent
14ed9c15d8
commit
401b02b680
13 files changed
+1582
No files matched your search
@@ -0,0 +1,113 @@
|
||||
"""Check generated content and export contact sheets for visual review."""
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
import argparse
|
||||
import pymupdf
|
||||
import openpyxl
|
||||
from PIL import Image, ImageDraw
|
||||
from pptx import Presentation
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument('--recalculated-workbook',type=Path,help='Optional independent LibreOffice/Excel recalculation copy')
|
||||
args=parser.parse_args()
|
||||
OUT=ROOT/'materials'
|
||||
PREV=OUT/'preview'
|
||||
PREV.mkdir(exist_ok=True)
|
||||
p=Presentation(OUT/'excel-ai-lecture.pptx')
|
||||
assert len(p.slides)==26
|
||||
manifest=json.loads((OUT/'slides-manifest.json').read_text(encoding='utf-8'))
|
||||
assert sum(s['seconds'] for s in manifest)==3600
|
||||
notes=(OUT/'speaker-notes.md').read_text(encoding='utf-8')
|
||||
durations=[int(v) for v in re.findall(r'\*\*Длительность:\*\* (\d+) секунд',notes)]
|
||||
assert durations==[s['seconds'] for s in manifest],durations
|
||||
assert all(s.notes_slide.notes_text_frame.text.strip() for s in p.slides)
|
||||
for n,s in enumerate(p.slides,1):
|
||||
for sh in s.shapes:
|
||||
assert sh.left>=0 and sh.top>=0,(n,sh.name,'negative position')
|
||||
assert sh.left+sh.width<=p.slide_width+10000,(n,sh.name,'too wide')
|
||||
assert sh.top+sh.height<=p.slide_height+10000,(n,sh.name,'too tall')
|
||||
|
||||
book=openpyxl.load_workbook(OUT/'excel-practice.xlsx',data_only=True)
|
||||
rows=list(book['Реестр'].iter_rows(min_row=2,max_row=61,values_only=True))
|
||||
assert len(rows)==60 and len({r[0] for r in rows})==60
|
||||
assert all(all(v is not None for v in r[:6]) for r in rows)
|
||||
assert Counter(r[7] for r in rows)=={'Завершено':24,'Просрочено':18,'В срок':18}
|
||||
assert sum(r[5]=='Выполнено' and r[6]>r[4] for r in rows)==12
|
||||
assert all(r[6] is None or r[6]<=book['Параметры']['B2'].value for r in rows)
|
||||
assert book['Проверка']['B6'].value==.5
|
||||
assert len(book['Реестр'].tables)==1
|
||||
assert len(book['Старт'].tables)==0
|
||||
formulas=openpyxl.load_workbook(OUT/'excel-practice.xlsx',data_only=False)
|
||||
assert all(formulas['Реестр'].cell(r,8).data_type=='f' for r in range(2,62))
|
||||
for r in rows:
|
||||
expected='Завершено' if r[5]=='Выполнено' else ('Просрочено' if r[4]<book['Параметры']['B2'].value else 'В срок')
|
||||
assert expected==r[7]
|
||||
assert all(book['Тесты'].cell(n,4).value==book['Тесты'].cell(n,5).value for n in range(2,6))
|
||||
recalc_status='not performed in this check'
|
||||
if args.recalculated_workbook:
|
||||
recalculated=openpyxl.load_workbook(args.recalculated_workbook,data_only=True)
|
||||
assert list(recalculated['Реестр'].iter_rows(min_row=2,max_row=61,values_only=True))==rows
|
||||
for n in range(2,10):
|
||||
assert recalculated['Проверка'].cell(n,2).value==book['Проверка'].cell(n,2).value
|
||||
assert all(recalculated['Тесты'].cell(n,4).value==recalculated['Тесты'].cell(n,5).value for n in range(2,6))
|
||||
recalc_status='independent recalculation matches all 60 rows, check totals and four boundary tests'
|
||||
pdf=pymupdf.open(OUT/'excel-ai-lecture.pdf')
|
||||
assert len(pdf)==26
|
||||
|
||||
def norm(t):
|
||||
return ''.join(c for c in unicodedata.normalize('NFKC',t).lower() if c.isalnum())
|
||||
|
||||
warnings=[]
|
||||
for i,s in enumerate(p.slides):
|
||||
page=pdf[i]
|
||||
pdf_text=norm(page.get_text())
|
||||
for sh in s.shapes:
|
||||
if not sh.has_text_frame:continue
|
||||
for para in sh.text_frame.paragraphs:
|
||||
expected=norm(para.text)
|
||||
if expected and expected not in pdf_text:
|
||||
warnings.append(f'Slide {i+1}: text not found intact: {para.text}')
|
||||
for block in page.get_text('dict')['blocks']:
|
||||
if 'lines' not in block:continue
|
||||
for line in block['lines']:
|
||||
for span in line['spans']:
|
||||
x0,y0,x1,y1=span['bbox']
|
||||
if min(x0,y0)<-1 or x1>page.rect.width+1 or y1>page.rect.height+1:
|
||||
warnings.append(f'Slide {i+1}: text exceeds page: {span["text"]}')
|
||||
page.get_pixmap(matrix=pymupdf.Matrix(1.4,1.4)).save(PREV/f'slide-{i+1:02}.png')
|
||||
|
||||
for start in range(0,26,8):
|
||||
ids=list(range(start,min(start+8,26)))
|
||||
sheet=Image.new('RGB',(1200,360*((len(ids)+1)//2)),'#dedee8')
|
||||
for k,i in enumerate(ids):
|
||||
pm=pdf[i].get_pixmap(matrix=pymupdf.Matrix(.625,.625))
|
||||
im=Image.frombytes('RGB',(pm.width,pm.height),pm.samples)
|
||||
x=(k%2)*600;y=(k//2)*360
|
||||
sheet.paste(im,(x,y))
|
||||
ImageDraw.Draw(sheet).text((x+12,y+339),f'SLIDE {i+1:02}',fill='black')
|
||||
sheet.save(PREV/f'contact-{start//8+1}.png')
|
||||
|
||||
try:
|
||||
import zxingcpp
|
||||
result=zxingcpp.read_barcode(Image.open(OUT/'telegram-qr.png'))
|
||||
assert result and result.text=='https://t.me/kirill_bouko_it'
|
||||
# Verify QR survives PDF export, not only its standalone source image.
|
||||
result=zxingcpp.read_barcode(Image.open(PREV/'slide-25.png'))
|
||||
assert result and result.text=='https://t.me/kirill_bouko_it'
|
||||
qr_status='decoded from PNG and PDF slide 25'
|
||||
except ImportError:
|
||||
qr_status='decoder unavailable; image generated from exact URL'
|
||||
|
||||
report={
|
||||
'slides':26,'total_seconds':3600,'notes_embedded':True,'pdf_pages':len(pdf),
|
||||
'synthetic_rows':60,'completed':24,'open':36,'overdue_open':18,'overdue_share_open':.5,
|
||||
'qr':qr_status,'render_text_warnings':warnings,
|
||||
'recalculated_workbook':recalc_status,
|
||||
}
|
||||
(OUT/'validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||
print(json.dumps(report,ensure_ascii=False,indent=2))
|
||||
assert not warnings,'Inspect PDF text warnings before delivering'
|
||||
Reference in new issue
Block a user