Spaces:
Running
Running
Henrique Braga commited on
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,1443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
# ══════════════════════════════════════════════════════════════
|
| 4 |
+
# ☢︎ RADIOTERAPIA.AI — POP de elite
|
| 5 |
+
# Célula 2: Aplicação completa
|
| 6 |
+
# por: Braga, HF.
|
| 7 |
+
# ══════════════════════════════════════════════════════════════
|
| 8 |
+
#
|
| 9 |
+
# • Landing page com navegação para Gemini Gem + App
|
| 10 |
+
# • Paleta de 4 cores clicáveis (primária cascateia, demais customizáveis)
|
| 11 |
+
# • Upload de logotipo institucional no cabeçalho
|
| 12 |
+
# • 10 tipos de SmartArt: checklist, escala, tabela, processo,
|
| 13 |
+
# fluxograma, ciclo, hierarquia, pirâmide, misto, texto
|
| 14 |
+
# • Preview de páginas com navegação ◄ ►
|
| 15 |
+
# • Download funcional via gr.DownloadButton
|
| 16 |
+
# • Cor padrão: #283264
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import json, os, tempfile, io
|
| 20 |
+
from datetime import datetime
|
| 21 |
+
try:
|
| 22 |
+
import graphviz as gv
|
| 23 |
+
HAS_GRAPHVIZ = True
|
| 24 |
+
except ImportError:
|
| 25 |
+
HAS_GRAPHVIZ = False
|
| 26 |
+
from docx import Document
|
| 27 |
+
from docx.shared import Pt, Cm, Emu, RGBColor, Inches
|
| 28 |
+
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
| 29 |
+
from docx.enum.table import WD_TABLE_ALIGNMENT
|
| 30 |
+
from docx.oxml.ns import qn, nsdecls
|
| 31 |
+
from docx.oxml import parse_xml
|
| 32 |
+
|
| 33 |
+
# ============================================================
|
| 34 |
+
# SISTEMA DE CORES DINÂMICO
|
| 35 |
+
# ============================================================
|
| 36 |
+
|
| 37 |
+
def hex_to_rgb(h):
|
| 38 |
+
h = h.lstrip("#")
|
| 39 |
+
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
| 40 |
+
|
| 41 |
+
def rgb_to_hex(r, g, b):
|
| 42 |
+
return f"{min(255,max(0,int(r))):02X}{min(255,max(0,int(g))):02X}{min(255,max(0,int(b))):02X}"
|
| 43 |
+
|
| 44 |
+
def lighten(hex_color, pct):
|
| 45 |
+
"""Clareia uma cor por pct% misturando com branco."""
|
| 46 |
+
r, g, b = hex_to_rgb(hex_color)
|
| 47 |
+
return rgb_to_hex(r + (255-r)*pct, g + (255-g)*pct, b + (255-b)*pct)
|
| 48 |
+
|
| 49 |
+
def darken(hex_color, pct):
|
| 50 |
+
r, g, b = hex_to_rgb(hex_color)
|
| 51 |
+
return rgb_to_hex(r*(1-pct), g*(1-pct), b*(1-pct))
|
| 52 |
+
|
| 53 |
+
def build_palette(primary_hex):
|
| 54 |
+
"""Gera paleta completa a partir de uma cor primária."""
|
| 55 |
+
p = primary_hex.lstrip("#").upper()
|
| 56 |
+
return {
|
| 57 |
+
"primary": p, # H1, bullets, table headers
|
| 58 |
+
"primary_dark": darken(p, 0.15), # Texto sobre fundo claro
|
| 59 |
+
"secondary": lighten(p, 0.45), # H2, SmartArt — +10% mais claro que v4.0
|
| 60 |
+
"tertiary": lighten(p, 0.70), # Footer header, zebra — +10% mais claro
|
| 61 |
+
"quaternary": lighten(p, 0.85), # Zebra alternada mais clara
|
| 62 |
+
"text_on_primary": "FFFFFF", # Texto sobre cor primária
|
| 63 |
+
"text_on_secondary": darken(p, 0.30), # Texto sobre cor secundária
|
| 64 |
+
"text_body": "1A1A1A",
|
| 65 |
+
"header_text": "000000", # PRETO automático para cabeçalho
|
| 66 |
+
"border": darken(p, 0.0),
|
| 67 |
+
"border_light": lighten(p, 0.20),
|
| 68 |
+
"risk_red": "8B1A1A",
|
| 69 |
+
"barrier_green": "1B5E20",
|
| 70 |
+
"gray_border": "7F8C9A",
|
| 71 |
+
"annex_border": "000000",
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
def parse_color_input(val):
|
| 75 |
+
"""Parseia qualquer formato de cor do Gradio ColorPicker → hex 6 chars."""
|
| 76 |
+
if not val:
|
| 77 |
+
return DEFAULT_PRIMARY
|
| 78 |
+
val = str(val).strip()
|
| 79 |
+
|
| 80 |
+
# Formato rgb(R, G, B) ou rgba(R, G, B, A) — COM FLOATS
|
| 81 |
+
if val.startswith("rgb"):
|
| 82 |
+
import re
|
| 83 |
+
# Capturar números com decimais: 248.402, 79.543, etc.
|
| 84 |
+
nums = re.findall(r'[\d]+\.?[\d]*', val)
|
| 85 |
+
if len(nums) >= 3:
|
| 86 |
+
r = min(255, max(0, int(float(nums[0]))))
|
| 87 |
+
g = min(255, max(0, int(float(nums[1]))))
|
| 88 |
+
b = min(255, max(0, int(float(nums[2]))))
|
| 89 |
+
return rgb_to_hex(r, g, b)
|
| 90 |
+
return DEFAULT_PRIMARY
|
| 91 |
+
|
| 92 |
+
# Formato hex
|
| 93 |
+
c = val.lstrip("#")
|
| 94 |
+
c = ''.join(ch for ch in c if ch in '0123456789abcdefABCDEF')
|
| 95 |
+
if len(c) >= 6:
|
| 96 |
+
return c[:6].upper()
|
| 97 |
+
if len(c) == 3:
|
| 98 |
+
return (c[0]*2 + c[1]*2 + c[2]*2).upper()
|
| 99 |
+
return DEFAULT_PRIMARY
|
| 100 |
+
|
| 101 |
+
DEFAULT_PRIMARY = "283264"
|
| 102 |
+
|
| 103 |
+
FONTE = "Calibri"
|
| 104 |
+
TAM_CORPO = Pt(11)
|
| 105 |
+
TAM_H1 = Pt(12)
|
| 106 |
+
TAM_H2 = Pt(11)
|
| 107 |
+
TAM_SMALL = Pt(9)
|
| 108 |
+
TAM_HEADER = Pt(9)
|
| 109 |
+
TAM_TINY = Pt(8)
|
| 110 |
+
|
| 111 |
+
# Página A4
|
| 112 |
+
MARGEM_SUP = Cm(5.0) # v4.1: aumentado para 5.0cm — mais respiro header↔corpo
|
| 113 |
+
MARGEM_INF = Cm(3.0)
|
| 114 |
+
MARGEM_ESQ = Cm(2.5)
|
| 115 |
+
MARGEM_DIR = Cm(2.0)
|
| 116 |
+
LARGURA_DXA = int((21 - 2.5 - 2.0) * 567)
|
| 117 |
+
|
| 118 |
+
# Indentação hierárquica
|
| 119 |
+
IND_H2 = 0.5
|
| 120 |
+
IND_BODY = 0.5
|
| 121 |
+
IND_ITEM = 1.0
|
| 122 |
+
IND_SUBITEM = 1.5
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# ============================================================
|
| 126 |
+
# UTILITÁRIOS DOCX
|
| 127 |
+
# ============================================================
|
| 128 |
+
|
| 129 |
+
def set_shading(cell, color):
|
| 130 |
+
cell._tc.get_or_add_tcPr().append(
|
| 131 |
+
parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color}" w:val="clear"/>'))
|
| 132 |
+
|
| 133 |
+
def set_borders(cell, top=None, bottom=None, left=None, right=None):
|
| 134 |
+
tcPr = cell._tc.get_or_add_tcPr()
|
| 135 |
+
borders = parse_xml(f'<w:tcBorders {nsdecls("w")}></w:tcBorders>')
|
| 136 |
+
for side, val in [("top",top),("bottom",bottom),("left",left),("right",right)]:
|
| 137 |
+
if val:
|
| 138 |
+
c, s = val if isinstance(val, tuple) else (val, "4")
|
| 139 |
+
borders.append(parse_xml(
|
| 140 |
+
f'<w:{side} {nsdecls("w")} w:val="single" w:sz="{s}" w:space="0" w:color="{c}"/>'))
|
| 141 |
+
tcPr.append(borders)
|
| 142 |
+
|
| 143 |
+
def set_valign(cell, v="center"):
|
| 144 |
+
cell._tc.get_or_add_tcPr().append(parse_xml(f'<w:vAlign {nsdecls("w")} w:val="{v}"/>'))
|
| 145 |
+
|
| 146 |
+
def set_width(cell, w):
|
| 147 |
+
cell._tc.get_or_add_tcPr().append(
|
| 148 |
+
parse_xml(f'<w:tcW {nsdecls("w")} w:w="{w}" w:type="dxa"/>'))
|
| 149 |
+
|
| 150 |
+
def set_margins(cell, t=0, b=0, l=80, r=80):
|
| 151 |
+
cell._tc.get_or_add_tcPr().append(parse_xml(
|
| 152 |
+
f'<w:tcMar {nsdecls("w")}><w:top w:w="{t}" w:type="dxa"/>'
|
| 153 |
+
f'<w:left w:w="{l}" w:type="dxa"/><w:bottom w:w="{b}" w:type="dxa"/>'
|
| 154 |
+
f'<w:right w:w="{r}" w:type="dxa"/></w:tcMar>'))
|
| 155 |
+
|
| 156 |
+
def fmt(p, text, bold=False, italic=False, size=None, color=None, font=FONTE, caps=False):
|
| 157 |
+
run = p.add_run(text)
|
| 158 |
+
run.bold = bold; run.italic = italic
|
| 159 |
+
if size: run.font.size = size
|
| 160 |
+
if color: run.font.color.rgb = RGBColor.from_string(color)
|
| 161 |
+
run.font.name = font
|
| 162 |
+
if caps: run.font.all_caps = True
|
| 163 |
+
return run
|
| 164 |
+
|
| 165 |
+
def spacing(p, before=0, after=0, line=1.15):
|
| 166 |
+
pf = p.paragraph_format
|
| 167 |
+
pf.space_before = Pt(before); pf.space_after = Pt(after); pf.line_spacing = line
|
| 168 |
+
|
| 169 |
+
def p_shading(p, color):
|
| 170 |
+
p._p.get_or_add_pPr().append(
|
| 171 |
+
parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color}" w:val="clear"/>'))
|
| 172 |
+
|
| 173 |
+
def repeat_header(row):
|
| 174 |
+
row._tr.get_or_add_trPr().append(parse_xml(f'<w:tblHeader {nsdecls("w")}/>'))
|
| 175 |
+
|
| 176 |
+
def page_break(doc):
|
| 177 |
+
p = doc.add_paragraph()
|
| 178 |
+
p.add_run()._r.append(parse_xml(f'<w:br {nsdecls("w")} w:type="page"/>'))
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
# ============================================================
|
| 182 |
+
# HEADER + FOOTER
|
| 183 |
+
# ============================================================
|
| 184 |
+
|
| 185 |
+
def build_header(section, meta, pal, logo_bytes=None):
|
| 186 |
+
header = section.header
|
| 187 |
+
header.is_linked_to_previous = False
|
| 188 |
+
for p in header.paragraphs: p.clear()
|
| 189 |
+
|
| 190 |
+
tbl = header.add_table(rows=4, cols=3, width=Cm(16.5))
|
| 191 |
+
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 192 |
+
cw = [1700, 5800, 2900]
|
| 193 |
+
brd = (pal["gray_border"], "4")
|
| 194 |
+
|
| 195 |
+
for row in tbl.rows:
|
| 196 |
+
for i, cell in enumerate(row.cells):
|
| 197 |
+
set_width(cell, cw[i]); set_valign(cell)
|
| 198 |
+
set_margins(cell, 30, 30, 80, 80)
|
| 199 |
+
set_borders(cell, brd, brd, brd, brd)
|
| 200 |
+
|
| 201 |
+
# Logo
|
| 202 |
+
logo_cell = tbl.cell(0,0).merge(tbl.cell(3,0))
|
| 203 |
+
logo_cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 204 |
+
if logo_bytes:
|
| 205 |
+
from docx.shared import Inches as In
|
| 206 |
+
run = logo_cell.paragraphs[0].add_run()
|
| 207 |
+
run.add_picture(io.BytesIO(logo_bytes), height=Cm(1.8))
|
| 208 |
+
else:
|
| 209 |
+
fmt(logo_cell.paragraphs[0], "[LOGO]", bold=True, size=Pt(10), color="888888")
|
| 210 |
+
|
| 211 |
+
# Título — PRETO
|
| 212 |
+
tc = tbl.cell(0,1).merge(tbl.cell(1,1))
|
| 213 |
+
tc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 214 |
+
fmt(tc.paragraphs[0], "PROCEDIMENTO OPERACIONAL PADRÃO",
|
| 215 |
+
bold=True, size=Pt(13), color="000000")
|
| 216 |
+
|
| 217 |
+
# Nome do processo — PRETO
|
| 218 |
+
pc = tbl.cell(2,1).merge(tbl.cell(3,1))
|
| 219 |
+
pc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 220 |
+
fmt(pc.paragraphs[0], meta.get("titulo_processo","").upper(),
|
| 221 |
+
bold=True, size=Pt(12), color="000000")
|
| 222 |
+
|
| 223 |
+
# Metadados — PRETO
|
| 224 |
+
for i, (lbl, val) in enumerate([
|
| 225 |
+
("Código: ", meta.get("codigo","POP-XXX-001")),
|
| 226 |
+
("Elaborado em: ", meta.get("data_elaboracao","")),
|
| 227 |
+
("Revisado em: ", meta.get("data_revisao","—")),
|
| 228 |
+
("Válido até: ", meta.get("validade","")),
|
| 229 |
+
]):
|
| 230 |
+
c = tbl.cell(i, 2)
|
| 231 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 232 |
+
fmt(c.paragraphs[0], lbl, bold=True, size=TAM_HEADER, color="000000")
|
| 233 |
+
fmt(c.paragraphs[0], val or "—", size=TAM_HEADER, color="000000")
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def build_footer(section, meta, pal):
|
| 237 |
+
footer = section.footer
|
| 238 |
+
footer.is_linked_to_previous = False
|
| 239 |
+
for p in footer.paragraphs: p.clear()
|
| 240 |
+
|
| 241 |
+
footer.add_paragraph() # separator
|
| 242 |
+
tbl = footer.add_table(rows=2, cols=3, width=Cm(16.5))
|
| 243 |
+
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 244 |
+
cw = [3500, 3500, 3400]
|
| 245 |
+
brd = (pal["gray_border"], "4")
|
| 246 |
+
|
| 247 |
+
for row in tbl.rows:
|
| 248 |
+
for i, cell in enumerate(row.cells):
|
| 249 |
+
set_width(cell, cw[i]); set_valign(cell)
|
| 250 |
+
set_margins(cell, 30, 30, 60, 60)
|
| 251 |
+
set_borders(cell, brd, brd, brd, brd)
|
| 252 |
+
|
| 253 |
+
for i, h in enumerate(["Elaborado por:", "Validado por:", "Aprovado por:"]):
|
| 254 |
+
c = tbl.cell(0, i)
|
| 255 |
+
set_shading(c, pal["tertiary"]) # Cor terciária no header do rodapé
|
| 256 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 257 |
+
fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color=pal["primary_dark"])
|
| 258 |
+
|
| 259 |
+
for i, key in enumerate(["elaborado_por", "validado_por", "aprovado_por"]):
|
| 260 |
+
c = tbl.cell(1, i)
|
| 261 |
+
person = meta.get(key, {})
|
| 262 |
+
nome = person.get("nome","") if isinstance(person, dict) else ""
|
| 263 |
+
cargo = person.get("cargo","") if isinstance(person, dict) else ""
|
| 264 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 265 |
+
if nome:
|
| 266 |
+
fmt(c.paragraphs[0], nome, bold=True, size=TAM_SMALL, color=pal["text_body"])
|
| 267 |
+
p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 268 |
+
fmt(p2, cargo, size=TAM_TINY, color="666666")
|
| 269 |
+
else:
|
| 270 |
+
fmt(c.paragraphs[0], "________________", size=TAM_SMALL, color="AAAAAA")
|
| 271 |
+
|
| 272 |
+
# Página
|
| 273 |
+
pp = footer.add_paragraph()
|
| 274 |
+
pp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
| 275 |
+
spacing(pp, 2, 0)
|
| 276 |
+
fmt(pp, "Página ", size=TAM_TINY, color="888888")
|
| 277 |
+
for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>',
|
| 278 |
+
f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>',
|
| 279 |
+
f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']:
|
| 280 |
+
pp.add_run()._r.append(parse_xml(x))
|
| 281 |
+
r = pp.add_run("1"); r.font.size = TAM_TINY; r.font.color.rgb = RGBColor.from_string("888888")
|
| 282 |
+
pp.add_run()._r.append(parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
|
| 283 |
+
fmt(pp, f" | {meta.get('codigo','POP-XXX-001')} | v{meta.get('versao','01')}",
|
| 284 |
+
size=TAM_TINY, color="888888")
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
# ============================================================
|
| 288 |
+
# ELEMENTOS DE CONTEÚDO
|
| 289 |
+
# ============================================================
|
| 290 |
+
|
| 291 |
+
def add_h1(doc, num, titulo, pal):
|
| 292 |
+
p = doc.add_paragraph(); spacing(p, 16, 8)
|
| 293 |
+
p_shading(p, pal["primary"])
|
| 294 |
+
pf = p.paragraph_format; pf.left_indent = Cm(0.3); pf.right_indent = Cm(0.3)
|
| 295 |
+
fmt(p, f" {num}. {titulo.upper()}", bold=True, size=TAM_H1, color=pal["text_on_primary"])
|
| 296 |
+
|
| 297 |
+
def add_h2(doc, num, titulo, pal):
|
| 298 |
+
p = doc.add_paragraph(); spacing(p, 12, 6)
|
| 299 |
+
p_shading(p, pal["secondary"])
|
| 300 |
+
pf = p.paragraph_format; pf.left_indent = Cm(IND_H2 + 0.3); pf.right_indent = Cm(0.3)
|
| 301 |
+
fmt(p, f" {num}. {titulo.upper()}", bold=True, size=TAM_H2, color=pal["text_on_secondary"])
|
| 302 |
+
|
| 303 |
+
def add_body(doc, text, indent=IND_BODY):
|
| 304 |
+
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 305 |
+
spacing(p, 2, 5, 1.15); p.paragraph_format.left_indent = Cm(indent)
|
| 306 |
+
fmt(p, text, size=TAM_CORPO, color="1A1A1A")
|
| 307 |
+
|
| 308 |
+
def add_bullet(doc, text, bold_prefix=None, indent=IND_ITEM, pal=None):
|
| 309 |
+
c = pal["primary"] if pal else DEFAULT_PRIMARY
|
| 310 |
+
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 311 |
+
spacing(p, 1, 3, 1.15)
|
| 312 |
+
p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.4)
|
| 313 |
+
fmt(p, "● ", size=Pt(9), color=c, bold=True)
|
| 314 |
+
if bold_prefix:
|
| 315 |
+
fmt(p, bold_prefix, bold=True, size=TAM_CORPO, color=c)
|
| 316 |
+
fmt(p, text, size=TAM_CORPO, color="1A1A1A")
|
| 317 |
+
else:
|
| 318 |
+
fmt(p, text, size=TAM_CORPO, color="1A1A1A")
|
| 319 |
+
|
| 320 |
+
def add_num(doc, n, text, indent=IND_SUBITEM, pal=None):
|
| 321 |
+
c = pal["primary"] if pal else DEFAULT_PRIMARY
|
| 322 |
+
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 323 |
+
spacing(p, 1, 4, 1.15)
|
| 324 |
+
p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.5)
|
| 325 |
+
fmt(p, f"{n}. ", bold=True, size=TAM_CORPO, color=c)
|
| 326 |
+
fmt(p, text, size=TAM_CORPO, color="1A1A1A")
|
| 327 |
+
|
| 328 |
+
def add_def_item(doc, termo, defn, pal):
|
| 329 |
+
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 330 |
+
spacing(p, 2, 5, 1.15)
|
| 331 |
+
p.paragraph_format.left_indent = Cm(IND_ITEM); p.paragraph_format.first_line_indent = Cm(-0.4)
|
| 332 |
+
fmt(p, "● ", size=Pt(9), color=pal["primary"], bold=True)
|
| 333 |
+
fmt(p, f"{termo}: ", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 334 |
+
fmt(p, defn, size=TAM_CORPO, color="1A1A1A")
|
| 335 |
+
|
| 336 |
+
def add_risk(doc, risco, barreira, pal):
|
| 337 |
+
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 338 |
+
spacing(p, 4, 3, 1.15)
|
| 339 |
+
p.paragraph_format.left_indent = Cm(IND_ITEM); p.paragraph_format.first_line_indent = Cm(-0.5)
|
| 340 |
+
fmt(p, "⚠ ", size=TAM_CORPO, color=pal["risk_red"], bold=True)
|
| 341 |
+
fmt(p, "Risco: ", bold=True, size=TAM_CORPO, color=pal["risk_red"])
|
| 342 |
+
fmt(p, risco, size=TAM_CORPO, color="1A1A1A")
|
| 343 |
+
p2 = doc.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 344 |
+
spacing(p2, 0, 8, 1.15); p2.paragraph_format.left_indent = Cm(IND_SUBITEM)
|
| 345 |
+
fmt(p2, "↳ Barreira de Segurança: ", bold=True, size=TAM_CORPO, color=pal["barrier_green"])
|
| 346 |
+
fmt(p2, barreira, size=TAM_CORPO, color="1A1A1A")
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
# ============================================================
|
| 350 |
+
# TABELAS
|
| 351 |
+
# ============================================================
|
| 352 |
+
|
| 353 |
+
def add_table(doc, headers, rows, col_widths=None, pal=None):
|
| 354 |
+
nc = len(headers)
|
| 355 |
+
if not col_widths: col_widths = [LARGURA_DXA // nc] * nc
|
| 356 |
+
tbl = doc.add_table(rows=1+len(rows), cols=nc)
|
| 357 |
+
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 358 |
+
brd = (pal["border_light"], "4") if pal else ("3B6AA0", "4")
|
| 359 |
+
brd2 = (pal["gray_border"], "2") if pal else ("7F8C9A", "2")
|
| 360 |
+
|
| 361 |
+
for i, h in enumerate(headers):
|
| 362 |
+
c = tbl.cell(0, i); set_shading(c, pal["primary"] if pal else DEFAULT_PRIMARY)
|
| 363 |
+
set_width(c, col_widths[i]); set_valign(c)
|
| 364 |
+
set_margins(c, 50, 50, 80, 80); set_borders(c, brd, brd, brd, brd)
|
| 365 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 366 |
+
fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color="FFFFFF")
|
| 367 |
+
repeat_header(tbl.rows[0])
|
| 368 |
+
|
| 369 |
+
for ri, rd in enumerate(rows):
|
| 370 |
+
for ci, val in enumerate(rd):
|
| 371 |
+
c = tbl.cell(ri+1, ci); set_width(c, col_widths[ci]); set_valign(c)
|
| 372 |
+
set_margins(c, 40, 40, 80, 80); set_borders(c, brd2, brd2, brd2, brd2)
|
| 373 |
+
a = WD_ALIGN_PARAGRAPH.LEFT if ci == 0 and nc > 2 else WD_ALIGN_PARAGRAPH.CENTER
|
| 374 |
+
c.paragraphs[0].alignment = a
|
| 375 |
+
fmt(c.paragraphs[0], str(val), size=TAM_SMALL, color="1A1A1A")
|
| 376 |
+
if ri % 2 == 0:
|
| 377 |
+
for ci in range(nc):
|
| 378 |
+
set_shading(tbl.cell(ri+1, ci), pal["tertiary"] if pal else "E8EFF7")
|
| 379 |
+
doc.add_paragraph()
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
# ============================================================
|
| 383 |
+
# SMARTART
|
| 384 |
+
# ============================================================
|
| 385 |
+
|
| 386 |
+
def add_process(doc, titulo, etapas, pal):
|
| 387 |
+
if not etapas: return
|
| 388 |
+
p = doc.add_paragraph(); spacing(p, 8, 6)
|
| 389 |
+
p.paragraph_format.left_indent = Cm(IND_BODY)
|
| 390 |
+
fmt(p, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 391 |
+
|
| 392 |
+
n = len(etapas); total = n*2-1; aw = 400
|
| 393 |
+
bw = (LARGURA_DXA - aw*(n-1)) // n
|
| 394 |
+
tbl = doc.add_table(rows=1, cols=total)
|
| 395 |
+
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 396 |
+
colors = [pal["primary"], pal["primary"], pal["primary"], pal["primary"]]
|
| 397 |
+
|
| 398 |
+
for ci in range(total):
|
| 399 |
+
c = tbl.cell(0, ci)
|
| 400 |
+
if ci % 2 == 0:
|
| 401 |
+
ei = ci // 2
|
| 402 |
+
set_width(c, bw); set_shading(c, pal["primary"] if ei % 2 == 0 else lighten(pal["primary"], 0.15))
|
| 403 |
+
bg = pal["primary"] if ei % 2 == 0 else lighten(pal["primary"], 0.15)
|
| 404 |
+
set_borders(c, (bg,"6"), (bg,"6"), (bg,"6"), (bg,"6"))
|
| 405 |
+
set_valign(c); set_margins(c, 60, 60, 80, 80)
|
| 406 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 407 |
+
fmt(c.paragraphs[0], f"Etapa {ei+1}", bold=True, size=TAM_TINY, color="FFFFFF")
|
| 408 |
+
p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 409 |
+
fmt(p2, etapas[ei], size=TAM_SMALL, color="FFFFFF")
|
| 410 |
+
else:
|
| 411 |
+
set_width(c, aw)
|
| 412 |
+
set_borders(c,("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"))
|
| 413 |
+
set_valign(c); c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 414 |
+
fmt(c.paragraphs[0], "→", bold=True, size=Pt(16), color=pal["primary"])
|
| 415 |
+
doc.add_paragraph()
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def add_checklist(doc, titulo, itens, pal):
|
| 419 |
+
if not itens: return
|
| 420 |
+
p = doc.add_paragraph(); spacing(p, 8, 6)
|
| 421 |
+
p.paragraph_format.left_indent = Cm(IND_BODY)
|
| 422 |
+
fmt(p, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 423 |
+
|
| 424 |
+
tbl = doc.add_table(rows=len(itens), cols=2)
|
| 425 |
+
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 426 |
+
chw = 600; tw = LARGURA_DXA - chw
|
| 427 |
+
|
| 428 |
+
for ri, item in enumerate(itens):
|
| 429 |
+
cc = tbl.cell(ri, 0); set_width(cc, chw); set_valign(cc)
|
| 430 |
+
set_margins(cc, 40, 40, 40, 40); set_shading(cc, pal["primary"])
|
| 431 |
+
set_borders(cc,("FFFFFF","2"),("FFFFFF","2"),(pal["primary"],"4"),("FFFFFF","2"))
|
| 432 |
+
cc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 433 |
+
fmt(cc.paragraphs[0], "✓", bold=True, size=Pt(14), color="FFFFFF")
|
| 434 |
+
|
| 435 |
+
ct = tbl.cell(ri, 1); set_width(ct, tw); set_valign(ct)
|
| 436 |
+
set_margins(ct, 50, 50, 120, 80)
|
| 437 |
+
bg = pal["quaternary"] if ri % 2 == 0 else "FFFFFF"
|
| 438 |
+
set_shading(ct, bg)
|
| 439 |
+
set_borders(ct,(pal["gray_border"],"2"),(pal["gray_border"],"2"),("FFFFFF","0"),(pal["gray_border"],"2"))
|
| 440 |
+
ct.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.LEFT
|
| 441 |
+
fmt(ct.paragraphs[0], item, size=TAM_CORPO, color="1A1A1A")
|
| 442 |
+
doc.add_paragraph()
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def add_cycle(doc, titulo, etapas, pal):
|
| 446 |
+
if not etapas: return
|
| 447 |
+
p = doc.add_paragraph(); spacing(p, 8, 6)
|
| 448 |
+
p.paragraph_format.left_indent = Cm(IND_BODY)
|
| 449 |
+
fmt(p, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 450 |
+
|
| 451 |
+
n = len(etapas); cpr = min(n, 4); rows_n = (n+cpr-1)//cpr
|
| 452 |
+
bw = LARGURA_DXA // cpr
|
| 453 |
+
tbl = doc.add_table(rows=rows_n, cols=cpr)
|
| 454 |
+
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 455 |
+
|
| 456 |
+
for idx, et in enumerate(etapas):
|
| 457 |
+
r, c_idx = idx//cpr, idx%cpr
|
| 458 |
+
c = tbl.cell(r, c_idx); set_width(c, bw)
|
| 459 |
+
bg = pal["primary"] if idx % 2 == 0 else lighten(pal["primary"], 0.15)
|
| 460 |
+
set_shading(c, bg)
|
| 461 |
+
set_borders(c,("FFFFFF","4"),("FFFFFF","4"),("FFFFFF","4"),("FFFFFF","4"))
|
| 462 |
+
set_valign(c); set_margins(c, 60, 60, 80, 80)
|
| 463 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 464 |
+
arrow = " →" if idx < n-1 else " ⟳"
|
| 465 |
+
fmt(c.paragraphs[0], f"{idx+1}{arrow}", bold=True, size=TAM_TINY, color="FFFFFF")
|
| 466 |
+
p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 467 |
+
fmt(p2, et, size=TAM_SMALL, color="FFFFFF")
|
| 468 |
+
|
| 469 |
+
for idx in range(n, rows_n*cpr):
|
| 470 |
+
c = tbl.cell(idx//cpr, idx%cpr)
|
| 471 |
+
set_borders(c,("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"))
|
| 472 |
+
doc.add_paragraph()
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
# ============================================================
|
| 476 |
+
# ANEXOS COM BORDA — 1 PÁGINA POR ANEXO, BORDA FULL-HEIGHT
|
| 477 |
+
# ============================================================
|
| 478 |
+
|
| 479 |
+
# Altura da caixa bordada em DXA (~14.5cm para caber com H1+descrição na mesma página)
|
| 480 |
+
ANNEX_BOX_HEIGHT_DXA = 8200
|
| 481 |
+
|
| 482 |
+
def _set_row_height(row, height_dxa):
|
| 483 |
+
"""Define altura mínima de uma linha de tabela."""
|
| 484 |
+
trPr = row._tr.get_or_add_trPr()
|
| 485 |
+
trPr.append(parse_xml(
|
| 486 |
+
f'<w:trHeight {nsdecls("w")} w:val="{height_dxa}" w:hRule="atLeast"/>'))
|
| 487 |
+
|
| 488 |
+
def _fill_cell_blank_lines(cell, n=8):
|
| 489 |
+
"""Adiciona linhas em branco ao final de uma célula para preenchimento vertical."""
|
| 490 |
+
for _ in range(n):
|
| 491 |
+
p = cell.add_paragraph()
|
| 492 |
+
p.paragraph_format.space_before = Pt(0)
|
| 493 |
+
p.paragraph_format.space_after = Pt(0)
|
| 494 |
+
|
| 495 |
+
def start_annex_border(doc, pal):
|
| 496 |
+
"""Cria container bordado com altura mínima full-page."""
|
| 497 |
+
tbl = doc.add_table(rows=1, cols=1)
|
| 498 |
+
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 499 |
+
c = tbl.cell(0, 0)
|
| 500 |
+
brd = (pal["annex_border"], "6")
|
| 501 |
+
set_borders(c, brd, brd, brd, brd)
|
| 502 |
+
set_width(c, LARGURA_DXA)
|
| 503 |
+
set_margins(c, 150, 150, 250, 250)
|
| 504 |
+
_set_row_height(tbl.rows[0], ANNEX_BOX_HEIGHT_DXA)
|
| 505 |
+
return c, tbl
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
# ============================================================
|
| 509 |
+
# GERADOR DE DIAGRAMAS VIA GRAPHVIZ (imagens PNG)
|
| 510 |
+
# ============================================================
|
| 511 |
+
|
| 512 |
+
def gerar_diagrama_png(tipo, conteudo, titulo, pal):
|
| 513 |
+
"""Gera PNG de diagrama profissional via Graphviz. Retorna path ou None."""
|
| 514 |
+
if not HAS_GRAPHVIZ:
|
| 515 |
+
return None
|
| 516 |
+
try:
|
| 517 |
+
dot = gv.Digraph(format='png')
|
| 518 |
+
dot.attr(dpi='120', bgcolor='transparent', margin='0.15', nodesep='0.4', ranksep='0.5')
|
| 519 |
+
node_style = {
|
| 520 |
+
'style': 'filled,rounded', 'shape': 'box', 'fontname': 'Arial',
|
| 521 |
+
'fontsize': '11', 'fillcolor': f'#{pal["primary"]}',
|
| 522 |
+
'fontcolor': '#FFFFFF', 'color': f'#{pal["primary_dark"]}', 'penwidth': '1.2',
|
| 523 |
+
'height': '0.5', 'margin': '0.12,0.06'
|
| 524 |
+
}
|
| 525 |
+
node_light = {**node_style, 'fillcolor': f'#{pal["secondary"]}',
|
| 526 |
+
'fontcolor': f'#{pal["text_on_secondary"]}'}
|
| 527 |
+
node_ter = {**node_style, 'fillcolor': f'#{pal["tertiary"]}',
|
| 528 |
+
'fontcolor': f'#{pal["primary_dark"]}'}
|
| 529 |
+
edge_style = {'color': f'#{pal["primary"]}', 'penwidth': '1.0', 'arrowsize': '0.6'}
|
| 530 |
+
|
| 531 |
+
if tipo in ("processo", "fluxograma"):
|
| 532 |
+
dot.attr(rankdir='LR', size='8,2!')
|
| 533 |
+
etapas = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 534 |
+
for i, et in enumerate(etapas[:6]):
|
| 535 |
+
st = node_style if i % 2 == 0 else node_light
|
| 536 |
+
dot.node(f'n{i}', et[:30], **st)
|
| 537 |
+
if i > 0:
|
| 538 |
+
dot.edge(f'n{i-1}', f'n{i}', **edge_style)
|
| 539 |
+
|
| 540 |
+
elif tipo == "hierarquia":
|
| 541 |
+
dot.attr(rankdir='TB', size='8,4.5!')
|
| 542 |
+
items = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 543 |
+
for i, item in enumerate(items):
|
| 544 |
+
if isinstance(item, dict):
|
| 545 |
+
lbl = item.get('titulo', '')[:30]
|
| 546 |
+
dot.node(f'g{i}', lbl, **node_style)
|
| 547 |
+
for j, sub in enumerate(item.get('subitens', [])):
|
| 548 |
+
dot.node(f'g{i}s{j}', sub[:25], **node_light)
|
| 549 |
+
dot.edge(f'g{i}', f'g{i}s{j}', **edge_style)
|
| 550 |
+
else:
|
| 551 |
+
dot.node(f'g{i}', str(item)[:30], **node_style)
|
| 552 |
+
if i > 0:
|
| 553 |
+
dot.edge(f'g{i-1}', f'g{i}', **edge_style)
|
| 554 |
+
|
| 555 |
+
elif tipo == "piramide":
|
| 556 |
+
dot.attr(rankdir='TB', size='7,5!')
|
| 557 |
+
items = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 558 |
+
for i, item in enumerate(items):
|
| 559 |
+
w = str(max(1.5, 3.5 - i * 0.4))
|
| 560 |
+
st = {**node_style, 'width': w, 'fixedsize': 'true', 'height': '0.45'}
|
| 561 |
+
if i > len(items) // 2:
|
| 562 |
+
st = {**node_ter, 'width': w, 'fixedsize': 'true', 'height': '0.45'}
|
| 563 |
+
dot.node(f'p{i}', str(item)[:30], **st)
|
| 564 |
+
if i > 0:
|
| 565 |
+
dot.edge(f'p{i-1}', f'p{i}', **edge_style, style='invis')
|
| 566 |
+
for i in range(len(items)):
|
| 567 |
+
dot.body.append(f' {{ rank=same; p{i} }}')
|
| 568 |
+
|
| 569 |
+
elif tipo == "ciclo":
|
| 570 |
+
dot.attr(rankdir='LR', size='8,3!') # Horizontal e compacto
|
| 571 |
+
etapas = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 572 |
+
n = len(etapas)
|
| 573 |
+
for i, et in enumerate(etapas):
|
| 574 |
+
st = node_style if i % 2 == 0 else node_light
|
| 575 |
+
dot.node(f'c{i}', et[:30], **st)
|
| 576 |
+
for i in range(n):
|
| 577 |
+
dot.edge(f'c{i}', f'c{(i+1)%n}', **edge_style)
|
| 578 |
+
|
| 579 |
+
else:
|
| 580 |
+
return None
|
| 581 |
+
|
| 582 |
+
out = os.path.join(tempfile.gettempdir(), f"diag_{tipo}_{id(conteudo)}")
|
| 583 |
+
return dot.render(out, cleanup=True)
|
| 584 |
+
except Exception:
|
| 585 |
+
return None
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
def _inserir_diagrama_na_celula(cell, img_path):
|
| 589 |
+
"""Insere imagem PNG do diagrama dentro de uma célula do docx."""
|
| 590 |
+
if not img_path or not os.path.exists(img_path):
|
| 591 |
+
return False
|
| 592 |
+
try:
|
| 593 |
+
p = cell.add_paragraph()
|
| 594 |
+
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 595 |
+
run = p.add_run()
|
| 596 |
+
run.add_picture(img_path, width=Cm(15))
|
| 597 |
+
return True
|
| 598 |
+
except Exception:
|
| 599 |
+
return False
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
def _inserir_diagrama_no_corpo(doc, img_path, legenda="", pal=None):
|
| 603 |
+
"""Insere imagem PNG de diagrama no corpo do documento (fora de tabela/célula).
|
| 604 |
+
Centralizado, com legenda em itálico abaixo."""
|
| 605 |
+
if not img_path or not os.path.exists(img_path):
|
| 606 |
+
return
|
| 607 |
+
try:
|
| 608 |
+
p = doc.add_paragraph()
|
| 609 |
+
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 610 |
+
p.paragraph_format.space_before = Pt(8)
|
| 611 |
+
p.paragraph_format.space_after = Pt(4)
|
| 612 |
+
run = p.add_run()
|
| 613 |
+
run.add_picture(img_path, width=Cm(14))
|
| 614 |
+
if legenda:
|
| 615 |
+
pl = doc.add_paragraph()
|
| 616 |
+
pl.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 617 |
+
pl.paragraph_format.space_before = Pt(2)
|
| 618 |
+
pl.paragraph_format.space_after = Pt(8)
|
| 619 |
+
fmt(pl, legenda, italic=True, size=Pt(9),
|
| 620 |
+
color=pal["primary"] if pal else "333333")
|
| 621 |
+
except Exception:
|
| 622 |
+
pass
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
def _render_passo(doc, num, item, pal):
|
| 626 |
+
"""Renderiza um item de procedimento: string (texto) ou dict (diagrama inline).
|
| 627 |
+
Aceita: {"diagrama": {"tipo":..., "titulo":..., "conteudo":...}}
|
| 628 |
+
Ou: {"tipo":..., "titulo":..., "conteudo":...}
|
| 629 |
+
"""
|
| 630 |
+
if isinstance(item, str):
|
| 631 |
+
add_num(doc, num, item, pal=pal)
|
| 632 |
+
elif isinstance(item, dict):
|
| 633 |
+
# Extrair dados do diagrama (com ou sem wrapper "diagrama")
|
| 634 |
+
diag = item.get("diagrama", None)
|
| 635 |
+
if diag is None and "tipo" in item:
|
| 636 |
+
diag = item # Formato direto sem wrapper
|
| 637 |
+
if diag is None:
|
| 638 |
+
# Dict sem formato reconhecido — renderizar como texto
|
| 639 |
+
add_num(doc, num, str(item), pal=pal)
|
| 640 |
+
return
|
| 641 |
+
tipo = diag.get("tipo", "processo")
|
| 642 |
+
titulo = diag.get("titulo", "Diagrama")
|
| 643 |
+
conteudo = diag.get("conteudo", [])
|
| 644 |
+
img_path = gerar_diagrama_png(tipo, conteudo, titulo, pal)
|
| 645 |
+
if img_path:
|
| 646 |
+
_inserir_diagrama_no_corpo(doc, img_path, titulo, pal)
|
| 647 |
+
else:
|
| 648 |
+
# Fallback: texto descritivo
|
| 649 |
+
items_txt = ', '.join(str(c) for c in conteudo) if isinstance(conteudo, list) else str(conteudo)
|
| 650 |
+
add_num(doc, num, f"[{titulo}]: {items_txt}", pal=pal)
|
| 651 |
+
|
| 652 |
+
|
| 653 |
+
def render_annex(doc, anexo, num, pal):
|
| 654 |
+
page_break(doc)
|
| 655 |
+
titulo = anexo.get("titulo", anexo) if isinstance(anexo, dict) else str(anexo)
|
| 656 |
+
add_h1(doc, f"ANEXO {num}", titulo.upper(), pal)
|
| 657 |
+
|
| 658 |
+
if isinstance(anexo, str):
|
| 659 |
+
add_body(doc, f"Conteúdo do {titulo} — a ser inserido pela instituição.")
|
| 660 |
+
return
|
| 661 |
+
|
| 662 |
+
if anexo.get("descricao"):
|
| 663 |
+
add_body(doc, anexo["descricao"])
|
| 664 |
+
|
| 665 |
+
tipo = anexo.get("tipo", "texto")
|
| 666 |
+
conteudo = anexo.get("conteudo", "")
|
| 667 |
+
|
| 668 |
+
# Criar container bordado com altura full-page
|
| 669 |
+
bc, border_tbl = start_annex_border(doc, pal)
|
| 670 |
+
|
| 671 |
+
# Para tipos de diagrama: tentar Graphviz primeiro
|
| 672 |
+
graphviz_ok = False
|
| 673 |
+
if tipo in ("processo", "fluxograma", "hierarquia", "piramide", "ciclo"):
|
| 674 |
+
img_path = gerar_diagrama_png(tipo, conteudo, titulo, pal)
|
| 675 |
+
if img_path:
|
| 676 |
+
_inserir_diagrama_na_celula(bc, img_path)
|
| 677 |
+
graphviz_ok = True
|
| 678 |
+
_fill_cell_blank_lines(bc, 6)
|
| 679 |
+
|
| 680 |
+
# === CHECKLIST ===
|
| 681 |
+
if graphviz_ok:
|
| 682 |
+
pass # Graphviz renderizou — pular SmartArt de tabela
|
| 683 |
+
elif tipo == "checklist":
|
| 684 |
+
itens = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 685 |
+
pt = bc.paragraphs[0]; pt.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
| 686 |
+
fmt(pt, f"▸ {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 687 |
+
inner = bc.add_table(rows=len(itens), cols=2)
|
| 688 |
+
chw = 500; tw = LARGURA_DXA - 900
|
| 689 |
+
for ri, item in enumerate(itens):
|
| 690 |
+
cc = inner.cell(ri, 0); set_width(cc, chw); set_valign(cc)
|
| 691 |
+
set_margins(cc, 30, 30, 30, 30); set_shading(cc, pal["primary"])
|
| 692 |
+
set_borders(cc,("FFFFFF","2"),("FFFFFF","2"),(pal["primary"],"4"),("FFFFFF","2"))
|
| 693 |
+
cc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 694 |
+
fmt(cc.paragraphs[0], "✓", bold=True, size=Pt(12), color="FFFFFF")
|
| 695 |
+
ct = inner.cell(ri, 1); set_width(ct, tw); set_valign(ct)
|
| 696 |
+
set_margins(ct, 40, 40, 100, 60)
|
| 697 |
+
set_shading(ct, pal["quaternary"] if ri % 2 == 0 else "FFFFFF")
|
| 698 |
+
set_borders(ct,(pal["gray_border"],"1"),(pal["gray_border"],"1"),("FFFFFF","0"),(pal["gray_border"],"1"))
|
| 699 |
+
fmt(ct.paragraphs[0], item, size=TAM_SMALL, color="1A1A1A")
|
| 700 |
+
_fill_cell_blank_lines(bc, max(1, 12 - len(itens)))
|
| 701 |
+
|
| 702 |
+
# === ESCALA / TABELA ===
|
| 703 |
+
elif tipo in ("escala", "tabela"):
|
| 704 |
+
headers = anexo.get("colunas", [])
|
| 705 |
+
rows_data = conteudo if isinstance(conteudo, list) else anexo.get("linhas", [])
|
| 706 |
+
if headers and rows_data:
|
| 707 |
+
bc.paragraphs[0].text = ""
|
| 708 |
+
nc = len(headers); cw_each = (LARGURA_DXA - 900) // nc
|
| 709 |
+
inner = bc.add_table(rows=1+len(rows_data), cols=nc)
|
| 710 |
+
for i, h in enumerate(headers):
|
| 711 |
+
c = inner.cell(0, i); set_shading(c, pal["primary"])
|
| 712 |
+
set_width(c, cw_each); set_valign(c); set_margins(c, 40, 40, 60, 60)
|
| 713 |
+
set_borders(c,(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4"),(pal["primary"],"4"))
|
| 714 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 715 |
+
fmt(c.paragraphs[0], h, bold=True, size=TAM_SMALL, color="FFFFFF")
|
| 716 |
+
for ri, rd in enumerate(rows_data):
|
| 717 |
+
for ci, val in enumerate(rd):
|
| 718 |
+
c = inner.cell(ri+1, ci); set_width(c, cw_each); set_valign(c)
|
| 719 |
+
set_margins(c, 30, 30, 60, 60)
|
| 720 |
+
set_borders(c,(pal["gray_border"],"1"),(pal["gray_border"],"1"),
|
| 721 |
+
(pal["gray_border"],"1"),(pal["gray_border"],"1"))
|
| 722 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 723 |
+
fmt(c.paragraphs[0], str(val), size=TAM_SMALL, color="1A1A1A")
|
| 724 |
+
if ri % 2 == 0:
|
| 725 |
+
for ci in range(nc): set_shading(inner.cell(ri+1, ci), pal["tertiary"])
|
| 726 |
+
_fill_cell_blank_lines(bc, max(1, 10 - len(rows_data)))
|
| 727 |
+
|
| 728 |
+
# === PROCESSO / FLUXOGRAMA ===
|
| 729 |
+
elif tipo in ("processo", "fluxograma"):
|
| 730 |
+
etapas = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 731 |
+
etapas = etapas[:6]
|
| 732 |
+
fmt(bc.paragraphs[0], f"▸ Fluxo do Processo", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 733 |
+
n = len(etapas); total = n*2-1; aw = 350
|
| 734 |
+
bw = (LARGURA_DXA - 900 - aw*(n-1)) // n
|
| 735 |
+
inner = bc.add_table(rows=1, cols=total)
|
| 736 |
+
for ci in range(total):
|
| 737 |
+
c = inner.cell(0, ci)
|
| 738 |
+
if ci % 2 == 0:
|
| 739 |
+
ei = ci // 2; set_width(c, bw)
|
| 740 |
+
bg = pal["primary"] if ei % 2 == 0 else lighten(pal["primary"], 0.15)
|
| 741 |
+
set_shading(c, bg); set_borders(c,(bg,"4"),(bg,"4"),(bg,"4"),(bg,"4"))
|
| 742 |
+
set_valign(c); set_margins(c, 50, 50, 60, 60)
|
| 743 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 744 |
+
fmt(c.paragraphs[0], f"Etapa {ei+1}", bold=True, size=TAM_TINY, color="FFFFFF")
|
| 745 |
+
p2 = c.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 746 |
+
fmt(p2, etapas[ei], size=Pt(8), color="FFFFFF")
|
| 747 |
+
else:
|
| 748 |
+
set_width(c, aw)
|
| 749 |
+
set_borders(c,("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"),("FFFFFF","0"))
|
| 750 |
+
set_valign(c); c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 751 |
+
fmt(c.paragraphs[0], "\u2192", bold=True, size=Pt(14), color=pal["primary"])
|
| 752 |
+
_fill_cell_blank_lines(bc, 15)
|
| 753 |
+
|
| 754 |
+
# === HIERARQUIA (organograma / classificação em níveis) ===
|
| 755 |
+
elif tipo == "hierarquia":
|
| 756 |
+
items = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 757 |
+
fmt(bc.paragraphs[0], f"\u25b8 {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 758 |
+
for item in items:
|
| 759 |
+
if isinstance(item, dict):
|
| 760 |
+
# Nível 1: box colorido
|
| 761 |
+
p1 = bc.add_paragraph()
|
| 762 |
+
p1.paragraph_format.space_before = Pt(6)
|
| 763 |
+
p1_shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{pal["primary"]}" w:val="clear"/>')
|
| 764 |
+
p1._p.get_or_add_pPr().append(p1_shading)
|
| 765 |
+
fmt(p1, f" {item.get('titulo', '')}", bold=True, size=TAM_CORPO, color="FFFFFF")
|
| 766 |
+
# Nível 2: subitens indentados com barra lateral
|
| 767 |
+
for sub in item.get("subitens", []):
|
| 768 |
+
p2 = bc.add_paragraph()
|
| 769 |
+
p2.paragraph_format.left_indent = Cm(1.0)
|
| 770 |
+
p2.paragraph_format.space_before = Pt(2)
|
| 771 |
+
p2_shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{pal["tertiary"]}" w:val="clear"/>')
|
| 772 |
+
p2._p.get_or_add_pPr().append(p2_shading)
|
| 773 |
+
fmt(p2, f" \u25b9 {sub}", size=TAM_SMALL, color=pal["primary_dark"])
|
| 774 |
+
elif isinstance(item, str):
|
| 775 |
+
p1 = bc.add_paragraph()
|
| 776 |
+
p1_shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{pal["secondary"]}" w:val="clear"/>')
|
| 777 |
+
p1._p.get_or_add_pPr().append(p1_shading)
|
| 778 |
+
fmt(p1, f" {item}", bold=True, size=TAM_SMALL, color=pal["primary_dark"])
|
| 779 |
+
_fill_cell_blank_lines(bc, max(1, 8 - len(items)))
|
| 780 |
+
|
| 781 |
+
# === PIRÂMIDE (de cima para baixo, estreito→largo) ===
|
| 782 |
+
elif tipo == "piramide":
|
| 783 |
+
items = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 784 |
+
fmt(bc.paragraphs[0], f"\u25b8 {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 785 |
+
n = len(items)
|
| 786 |
+
max_w = LARGURA_DXA - 900
|
| 787 |
+
for i, item in enumerate(items):
|
| 788 |
+
# Cada nível fica mais largo (proporção crescente)
|
| 789 |
+
level_w = int(max_w * (0.4 + 0.6 * i / max(n-1, 1)))
|
| 790 |
+
margin_l = (max_w - level_w) // 2
|
| 791 |
+
inner = bc.add_table(rows=1, cols=1)
|
| 792 |
+
inner.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 793 |
+
c = inner.cell(0, 0)
|
| 794 |
+
set_width(c, level_w)
|
| 795 |
+
# Gradiente de cores: topo=primário, base=terciário
|
| 796 |
+
frac = i / max(n-1, 1)
|
| 797 |
+
r1, g1, b1 = hex_to_rgb(pal["primary"])
|
| 798 |
+
r2, g2, b2 = hex_to_rgb(pal["tertiary"])
|
| 799 |
+
bg = rgb_to_hex(r1+(r2-r1)*frac, g1+(g2-g1)*frac, b1+(b2-b1)*frac)
|
| 800 |
+
set_shading(c, bg)
|
| 801 |
+
brd = (bg, "4")
|
| 802 |
+
set_borders(c, brd, brd, brd, brd)
|
| 803 |
+
set_valign(c); set_margins(c, 40, 40, 80, 80)
|
| 804 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 805 |
+
txt_color = "FFFFFF" if frac < 0.5 else pal["primary_dark"]
|
| 806 |
+
fmt(c.paragraphs[0], str(item), bold=True, size=TAM_SMALL, color=txt_color)
|
| 807 |
+
_fill_cell_blank_lines(bc, max(1, 10 - n))
|
| 808 |
+
|
| 809 |
+
# === CICLO (PDCA, melhoria contínua) ===
|
| 810 |
+
elif tipo == "ciclo":
|
| 811 |
+
etapas = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 812 |
+
fmt(bc.paragraphs[0], f"\u25b8 {titulo}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 813 |
+
n = len(etapas)
|
| 814 |
+
# Layout em grid 2x2 ou linear
|
| 815 |
+
if n == 4:
|
| 816 |
+
inner = bc.add_table(rows=2, cols=2)
|
| 817 |
+
inner.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 818 |
+
positions = [(0,0),(0,1),(1,1),(1,0)] # Sentido horário
|
| 819 |
+
colors = [pal["primary"], lighten(pal["primary"],0.15),
|
| 820 |
+
lighten(pal["primary"],0.30), lighten(pal["primary"],0.45)]
|
| 821 |
+
bw = (LARGURA_DXA - 900) // 2
|
| 822 |
+
for idx, (r,c_idx) in enumerate(positions):
|
| 823 |
+
c = inner.cell(r, c_idx); set_width(c, bw); set_valign(c)
|
| 824 |
+
set_shading(c, colors[idx])
|
| 825 |
+
brd = (colors[idx], "4")
|
| 826 |
+
set_borders(c, brd, brd, brd, brd)
|
| 827 |
+
set_margins(c, 50, 50, 80, 80)
|
| 828 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 829 |
+
fmt(c.paragraphs[0], etapas[idx], bold=True, size=TAM_SMALL, color="FFFFFF")
|
| 830 |
+
# Setas no centro
|
| 831 |
+
p_arrow = bc.add_paragraph()
|
| 832 |
+
p_arrow.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 833 |
+
fmt(p_arrow, "\u21bb Ciclo Cont\u00ednuo", bold=True, size=TAM_SMALL, color=pal["primary"])
|
| 834 |
+
else:
|
| 835 |
+
cpr = min(n, 4); rows_n = (n+cpr-1)//cpr
|
| 836 |
+
bw = (LARGURA_DXA - 900) // cpr
|
| 837 |
+
inner = bc.add_table(rows=rows_n, cols=cpr)
|
| 838 |
+
inner.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 839 |
+
for idx, et in enumerate(etapas):
|
| 840 |
+
r, ci2 = divmod(idx, cpr)
|
| 841 |
+
c = inner.cell(r, ci2); set_width(c, bw); set_valign(c)
|
| 842 |
+
bg = pal["primary"] if idx % 2 == 0 else lighten(pal["primary"], 0.20)
|
| 843 |
+
set_shading(c, bg)
|
| 844 |
+
brd = (bg, "4")
|
| 845 |
+
set_borders(c, brd, brd, brd, brd)
|
| 846 |
+
set_margins(c, 40, 40, 60, 60)
|
| 847 |
+
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 848 |
+
fmt(c.paragraphs[0], et, bold=True, size=TAM_TINY, color="FFFFFF")
|
| 849 |
+
_fill_cell_blank_lines(bc, 10)
|
| 850 |
+
|
| 851 |
+
# === MISTO ===
|
| 852 |
+
elif tipo == "misto":
|
| 853 |
+
bc.paragraphs[0].text = ""
|
| 854 |
+
elementos = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 855 |
+
for elem in elementos:
|
| 856 |
+
if isinstance(elem, str):
|
| 857 |
+
pe = bc.add_paragraph(); fmt(pe, elem, size=TAM_CORPO, color="1A1A1A")
|
| 858 |
+
elif isinstance(elem, dict):
|
| 859 |
+
st = elem.get("tipo", "texto")
|
| 860 |
+
if st == "texto":
|
| 861 |
+
pe = bc.add_paragraph(); fmt(pe, elem.get("conteudo",""), size=TAM_CORPO, color="1A1A1A")
|
| 862 |
+
elif st == "checklist":
|
| 863 |
+
pe = bc.add_paragraph()
|
| 864 |
+
fmt(pe, f"\n\u25b8 {elem.get('titulo','')}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 865 |
+
for it in elem.get("itens", []):
|
| 866 |
+
pi = bc.add_paragraph(); pi.paragraph_format.left_indent = Cm(0.5)
|
| 867 |
+
fmt(pi, "\u2713 ", bold=True, size=TAM_SMALL, color=pal["primary"])
|
| 868 |
+
fmt(pi, it, size=TAM_SMALL, color="1A1A1A")
|
| 869 |
+
elif st == "processo":
|
| 870 |
+
pe = bc.add_paragraph()
|
| 871 |
+
fmt(pe, f"\n\u25b8 {elem.get('titulo','')}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 872 |
+
for i, et in enumerate(elem.get("etapas",[]), 1):
|
| 873 |
+
pi = bc.add_paragraph(); pi.paragraph_format.left_indent = Cm(0.5)
|
| 874 |
+
fmt(pi, f"{i}. ", bold=True, size=TAM_SMALL, color=pal["primary"])
|
| 875 |
+
fmt(pi, et, size=TAM_SMALL, color="1A1A1A")
|
| 876 |
+
elif st == "tabela":
|
| 877 |
+
pe = bc.add_paragraph()
|
| 878 |
+
fmt(pe, f"\n\u25b8 {elem.get('titulo','')}", bold=True, size=TAM_CORPO, color=pal["primary"])
|
| 879 |
+
hdrs = elem.get("colunas",[]); rws = elem.get("linhas",[])
|
| 880 |
+
if hdrs and rws:
|
| 881 |
+
nc2 = len(hdrs); cw2 = (LARGURA_DXA - 900) // nc2
|
| 882 |
+
it2 = bc.add_table(rows=1+len(rws), cols=nc2)
|
| 883 |
+
for i, h in enumerate(hdrs):
|
| 884 |
+
cc2 = it2.cell(0,i); set_shading(cc2, pal["primary"])
|
| 885 |
+
set_width(cc2,cw2); set_valign(cc2); set_margins(cc2,30,30,50,50)
|
| 886 |
+
set_borders(cc2,(pal["primary"],"3"),(pal["primary"],"3"),
|
| 887 |
+
(pal["primary"],"3"),(pal["primary"],"3"))
|
| 888 |
+
cc2.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 889 |
+
fmt(cc2.paragraphs[0], h, bold=True, size=TAM_TINY, color="FFFFFF")
|
| 890 |
+
for ri2, rd2 in enumerate(rws):
|
| 891 |
+
for ci2, v2 in enumerate(rd2):
|
| 892 |
+
cc2 = it2.cell(ri2+1,ci2); set_width(cc2,cw2); set_valign(cc2)
|
| 893 |
+
set_margins(cc2,25,25,50,50)
|
| 894 |
+
set_borders(cc2,(pal["gray_border"],"1"),(pal["gray_border"],"1"),
|
| 895 |
+
(pal["gray_border"],"1"),(pal["gray_border"],"1"))
|
| 896 |
+
cc2.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 897 |
+
fmt(cc2.paragraphs[0], str(v2), size=TAM_TINY, color="1A1A1A")
|
| 898 |
+
if ri2 % 2 == 0:
|
| 899 |
+
for ci2 in range(nc2): set_shading(it2.cell(ri2+1,ci2), pal["tertiary"])
|
| 900 |
+
_fill_cell_blank_lines(bc, 4)
|
| 901 |
+
|
| 902 |
+
# === TEXTO / FALLBACK ===
|
| 903 |
+
else:
|
| 904 |
+
if isinstance(conteudo, str):
|
| 905 |
+
fmt(bc.paragraphs[0], conteudo, size=TAM_CORPO, color="1A1A1A")
|
| 906 |
+
elif isinstance(conteudo, list):
|
| 907 |
+
bc.paragraphs[0].text = ""
|
| 908 |
+
for item in conteudo:
|
| 909 |
+
pi = bc.add_paragraph(); fmt(pi, f"\u25cf {item}", size=TAM_CORPO, color="1A1A1A")
|
| 910 |
+
_fill_cell_blank_lines(bc, 12)
|
| 911 |
+
|
| 912 |
+
|
| 913 |
+
# ============================================================
|
| 914 |
+
# GERADOR PRINCIPAL
|
| 915 |
+
# ============================================================
|
| 916 |
+
|
| 917 |
+
def gerar_pop_docx(json_str, primary_color=None, logo_bytes=None, palette_overrides=None):
|
| 918 |
+
data = json.loads(json_str)
|
| 919 |
+
meta = data.get("metadata", {})
|
| 920 |
+
secoes = data.get("secoes", {})
|
| 921 |
+
pal = build_palette(parse_color_input(primary_color))
|
| 922 |
+
# Aplicar overrides individuais de cor
|
| 923 |
+
if palette_overrides:
|
| 924 |
+
for k, v in palette_overrides.items():
|
| 925 |
+
if v and v.strip():
|
| 926 |
+
pal[k] = parse_color_input(v)
|
| 927 |
+
|
| 928 |
+
doc = Document()
|
| 929 |
+
style = doc.styles["Normal"]
|
| 930 |
+
style.font.name = FONTE; style.font.size = TAM_CORPO
|
| 931 |
+
style.font.color.rgb = RGBColor.from_string("1A1A1A")
|
| 932 |
+
style.paragraph_format.space_after = Pt(4); style.paragraph_format.line_spacing = 1.15
|
| 933 |
+
|
| 934 |
+
sec = doc.sections[0]
|
| 935 |
+
sec.page_width = Cm(21); sec.page_height = Cm(29.7)
|
| 936 |
+
sec.top_margin = MARGEM_SUP; sec.bottom_margin = MARGEM_INF
|
| 937 |
+
sec.left_margin = MARGEM_ESQ; sec.right_margin = MARGEM_DIR
|
| 938 |
+
|
| 939 |
+
build_header(sec, meta, pal, logo_bytes)
|
| 940 |
+
build_footer(sec, meta, pal)
|
| 941 |
+
|
| 942 |
+
if doc.paragraphs and doc.paragraphs[0].text == "":
|
| 943 |
+
doc.paragraphs[0]._element.getparent().remove(doc.paragraphs[0]._element)
|
| 944 |
+
|
| 945 |
+
# Seções
|
| 946 |
+
sn = 1
|
| 947 |
+
add_h1(doc, sn, "OBJETIVO / FINALIDADE", pal)
|
| 948 |
+
add_body(doc, secoes.get("objetivo", "[Objetivo não fornecido]"))
|
| 949 |
+
|
| 950 |
+
sn += 1; add_h1(doc, sn, "CAMPO DE APLICAÇÃO / ÁREA", pal)
|
| 951 |
+
add_body(doc, secoes.get("campo_aplicacao", "[Campo não fornecido]"))
|
| 952 |
+
|
| 953 |
+
sn += 1; add_h1(doc, sn, "CONCEITOS E DEFINIÇÕES", pal)
|
| 954 |
+
for it in secoes.get("conceitos", []):
|
| 955 |
+
if isinstance(it, dict): add_def_item(doc, it.get("termo",""), it.get("definicao",""), pal)
|
| 956 |
+
else: add_bullet(doc, str(it), pal=pal)
|
| 957 |
+
|
| 958 |
+
sn += 1; add_h1(doc, sn, "RESPONSABILIDADES E COMPETÊNCIAS", pal)
|
| 959 |
+
for it in secoes.get("responsabilidades", []):
|
| 960 |
+
if isinstance(it, dict):
|
| 961 |
+
add_bullet(doc, it.get("acoes",""), bold_prefix=f"{it.get('papel','')}: ", pal=pal)
|
| 962 |
+
else: add_bullet(doc, str(it), pal=pal)
|
| 963 |
+
|
| 964 |
+
sn += 1; add_h1(doc, sn, "RECURSOS NECESSÁRIOS (MATERIAIS E EQUIPAMENTOS)", pal)
|
| 965 |
+
for it in secoes.get("recursos", []):
|
| 966 |
+
add_bullet(doc, str(it), pal=pal)
|
| 967 |
+
|
| 968 |
+
sn += 1; add_h1(doc, sn, "DESCRIÇÃO DO PROCEDIMENTO (PASSO A PASSO)", pal)
|
| 969 |
+
proc = secoes.get("procedimento", {})
|
| 970 |
+
sub = 1
|
| 971 |
+
add_h2(doc, f"{sn}.{sub}", "Ações Iniciais e Preparo", pal)
|
| 972 |
+
for i, p in enumerate(proc.get("acoes_iniciais",[]), 1): _render_passo(doc, i, p, pal)
|
| 973 |
+
sub += 1; add_h2(doc, f"{sn}.{sub}", "Execução Técnica", pal)
|
| 974 |
+
for i, p in enumerate(proc.get("execucao_tecnica",[]), 1): _render_passo(doc, i, p, pal)
|
| 975 |
+
sub += 1; add_h2(doc, f"{sn}.{sub}", "Ações Finais e Organização", pal)
|
| 976 |
+
for i, p in enumerate(proc.get("acoes_finais",[]), 1): _render_passo(doc, i, p, pal)
|
| 977 |
+
|
| 978 |
+
sn += 1; add_h1(doc, sn, "GERENCIAMENTO DE RISCO E PONTOS CRÍTICOS", pal)
|
| 979 |
+
riscos = secoes.get("riscos", {})
|
| 980 |
+
add_h2(doc, f"{sn}.1", "Riscos Assistenciais", pal)
|
| 981 |
+
for it in riscos.get("assistenciais", []):
|
| 982 |
+
if isinstance(it, dict): add_risk(doc, it.get("risco",""), it.get("barreira",""), pal)
|
| 983 |
+
else: add_bullet(doc, str(it), pal=pal)
|
| 984 |
+
add_h2(doc, f"{sn}.2", "Plano de Contingência", pal)
|
| 985 |
+
add_body(doc, riscos.get("contingencia","[Contingência não fornecida]"), indent=IND_SUBITEM)
|
| 986 |
+
|
| 987 |
+
sn += 1; add_h1(doc, sn, "REGISTROS DA QUALIDADE (EVIDÊNCIAS)", pal)
|
| 988 |
+
add_body(doc, "A execução deste procedimento e eventuais intercorrências devem ser "
|
| 989 |
+
"obrigatoriamente documentadas nos seguintes registros para rastreabilidade e auditoria:")
|
| 990 |
+
for it in secoes.get("registros",[]): add_bullet(doc, str(it), pal=pal)
|
| 991 |
+
|
| 992 |
+
sn += 1; add_h1(doc, sn, "INDICADORES DE MONITORAMENTO", pal)
|
| 993 |
+
add_body(doc, "A eficácia da adesão a este POP será medida através dos seguintes indicadores institucionais:")
|
| 994 |
+
inds = secoes.get("indicadores", [])
|
| 995 |
+
if inds and isinstance(inds[0], dict) and "meta" in inds[0]:
|
| 996 |
+
add_table(doc, ["Indicador","Meta","Periodicidade"],
|
| 997 |
+
[[i.get("nome",""),i.get("meta",""),i.get("periodicidade","")] for i in inds],
|
| 998 |
+
[5157,2400,1800], pal)
|
| 999 |
+
else:
|
| 1000 |
+
for it in inds: add_bullet(doc, str(it), pal=pal)
|
| 1001 |
+
|
| 1002 |
+
sn += 1; add_h1(doc, sn, "REFERÊNCIAS BIBLIOGRÁFICAS / NORMATIVAS", pal)
|
| 1003 |
+
for i, ref in enumerate(secoes.get("referencias",[]), 1):
|
| 1004 |
+
add_num(doc, i, str(ref), indent=IND_ITEM, pal=pal)
|
| 1005 |
+
|
| 1006 |
+
sn += 1; add_h1(doc, sn, "ANEXOS / FLUXOGRAMAS", pal)
|
| 1007 |
+
anexos = secoes.get("anexos", [])
|
| 1008 |
+
if anexos:
|
| 1009 |
+
for idx, it in enumerate(anexos, 1):
|
| 1010 |
+
nome = it.get("titulo", it) if isinstance(it, dict) else str(it)
|
| 1011 |
+
add_bullet(doc, f"Anexo {idx}: {nome}", pal=pal)
|
| 1012 |
+
else:
|
| 1013 |
+
add_body(doc, "Não há anexos vinculados a este POP nesta versão.")
|
| 1014 |
+
|
| 1015 |
+
sn += 1; add_h1(doc, sn, "HISTÓRICO DE REVISÕES", pal)
|
| 1016 |
+
revs = secoes.get("historico_revisoes", [])
|
| 1017 |
+
if revs:
|
| 1018 |
+
add_table(doc, ["Versão","Data","Descrição da Alteração","Responsável"],
|
| 1019 |
+
[[r.get("versao",""),r.get("data",""),r.get("descricao",""),r.get("responsavel","")] for r in revs],
|
| 1020 |
+
[1000,1200,5557,1600], pal)
|
| 1021 |
+
|
| 1022 |
+
# Anexos completos
|
| 1023 |
+
if anexos:
|
| 1024 |
+
for idx, anx in enumerate(anexos, 1):
|
| 1025 |
+
render_annex(doc, anx, idx, pal)
|
| 1026 |
+
|
| 1027 |
+
titulo_safe = meta.get("titulo_processo","POP").replace(" ","_").replace("/","-")
|
| 1028 |
+
codigo = meta.get("codigo","POP-XXX-001")
|
| 1029 |
+
fp = os.path.join(tempfile.gettempdir(), f"{codigo}_{titulo_safe}.docx")
|
| 1030 |
+
doc.save(fp)
|
| 1031 |
+
return fp
|
| 1032 |
+
|
| 1033 |
+
|
| 1034 |
+
# ============================================================
|
| 1035 |
+
# VALIDADOR
|
| 1036 |
+
# ============================================================
|
| 1037 |
+
|
| 1038 |
+
def validar(json_str):
|
| 1039 |
+
try: data = json.loads(json_str)
|
| 1040 |
+
except json.JSONDecodeError as e: return False, f"❌ JSON inválido: {e}", None
|
| 1041 |
+
erros, avisos = [], []
|
| 1042 |
+
if "metadata" not in data: erros.append("'metadata' ausente")
|
| 1043 |
+
else:
|
| 1044 |
+
for c in ["titulo_processo","codigo","versao","setor"]:
|
| 1045 |
+
if not data["metadata"].get(c): erros.append(f"metadata.{c} vazio")
|
| 1046 |
+
if "secoes" not in data: erros.append("'secoes' ausente")
|
| 1047 |
+
else:
|
| 1048 |
+
for s in ["objetivo","campo_aplicacao","conceitos","responsabilidades","recursos",
|
| 1049 |
+
"procedimento","riscos","registros","indicadores","referencias","historico_revisoes"]:
|
| 1050 |
+
if s not in data["secoes"]: erros.append(f"Seção '{s}' ausente")
|
| 1051 |
+
if erros:
|
| 1052 |
+
return False, "❌ ERROS:\n"+"\n".join(f" • {e}" for e in erros), None
|
| 1053 |
+
msg = "✅ JSON válido!"
|
| 1054 |
+
if avisos: msg += "\n⚠️ "+"\n".join(avisos)
|
| 1055 |
+
return True, msg, data
|
| 1056 |
+
|
| 1057 |
+
|
| 1058 |
+
|
| 1059 |
+
# ============================================================
|
| 1060 |
+
# GRADIO FRONTEND
|
| 1061 |
+
# ============================================================
|
| 1062 |
+
|
| 1063 |
+
DEFAULT_UI_COLOR = "283264"
|
| 1064 |
+
|
| 1065 |
+
def processar(json_text, json_file, c_pri, c_sec, c_ter, c_zeb, logo_file):
|
| 1066 |
+
json_str = ""
|
| 1067 |
+
# Prioridade 1: arquivo anexo
|
| 1068 |
+
if json_file is not None:
|
| 1069 |
+
try:
|
| 1070 |
+
fpath = json_file.name if hasattr(json_file, 'name') else json_file
|
| 1071 |
+
with open(fpath, "r", encoding="utf-8") as f: json_str = f.read()
|
| 1072 |
+
except Exception as e:
|
| 1073 |
+
return None, f"\u274c {e}", None
|
| 1074 |
+
# Prioridade 2: texto colado
|
| 1075 |
+
if not json_str.strip():
|
| 1076 |
+
json_str = json_text or ""
|
| 1077 |
+
if not json_str.strip():
|
| 1078 |
+
return None, "\u26a0\ufe0f Insira o c\u00f3digo.", None
|
| 1079 |
+
jc = json_str.strip()
|
| 1080 |
+
if jc.startswith("```"): jc = jc.split("\n",1)[1] if "\n" in jc else jc[3:]
|
| 1081 |
+
if jc.endswith("```"): jc = jc[:-3]
|
| 1082 |
+
jc = jc.strip()
|
| 1083 |
+
ok, msg, data = validar(jc)
|
| 1084 |
+
if not ok: return None, msg, None
|
| 1085 |
+
pri = parse_color_input(c_pri) if c_pri else DEFAULT_UI_COLOR
|
| 1086 |
+
pal = build_palette(pri)
|
| 1087 |
+
if c_sec and c_sec.strip(): pal["secondary"] = parse_color_input(c_sec)
|
| 1088 |
+
if c_ter and c_ter.strip(): pal["tertiary"] = parse_color_input(c_ter)
|
| 1089 |
+
if c_zeb and c_zeb.strip(): pal["quaternary"] = parse_color_input(c_zeb)
|
| 1090 |
+
logo_bytes = None
|
| 1091 |
+
if logo_file is not None:
|
| 1092 |
+
try:
|
| 1093 |
+
lp = logo_file if isinstance(logo_file, str) else (logo_file.name if hasattr(logo_file,'name') else None)
|
| 1094 |
+
if lp and os.path.exists(lp):
|
| 1095 |
+
with open(lp, "rb") as f: logo_bytes = f.read()
|
| 1096 |
+
except: pass
|
| 1097 |
+
try:
|
| 1098 |
+
overrides = {}
|
| 1099 |
+
if c_sec and c_sec.strip(): overrides["secondary"] = c_sec
|
| 1100 |
+
if c_ter and c_ter.strip(): overrides["tertiary"] = c_ter
|
| 1101 |
+
if c_zeb and c_zeb.strip(): overrides["quaternary"] = c_zeb
|
| 1102 |
+
fp = gerar_pop_docx(jc, pri, logo_bytes, palette_overrides=overrides)
|
| 1103 |
+
meta = data.get("metadata",{})
|
| 1104 |
+
info = "\u2705 POP gerado com sucesso!"
|
| 1105 |
+
return fp, info, meta
|
| 1106 |
+
except Exception as e:
|
| 1107 |
+
return None, f"\u274c {str(e)}", None
|
| 1108 |
+
|
| 1109 |
+
|
| 1110 |
+
def criar_interface():
|
| 1111 |
+
try: import gradio as gr
|
| 1112 |
+
except: os.system("pip install gradio -q"); import gradio as gr
|
| 1113 |
+
|
| 1114 |
+
TITLE_COLOR = "#9ab4d2"
|
| 1115 |
+
init_pal = build_palette(DEFAULT_UI_COLOR)
|
| 1116 |
+
|
| 1117 |
+
with gr.Blocks(
|
| 1118 |
+
title="\u2622\ufe0e RADIOTERAPIA.AI - POP de elite",
|
| 1119 |
+
theme=gr.themes.Soft(primary_hue="blue"),
|
| 1120 |
+
css=f"""
|
| 1121 |
+
.gradio-container {{ max-width: 1200px !important; }}
|
| 1122 |
+
#pop-hex-pri, #pop-hex-sec, #pop-hex-ter, #pop-hex-zeb {{
|
| 1123 |
+
height: 0 !important; overflow: hidden !important;
|
| 1124 |
+
margin: 0 !important; padding: 0 !important;
|
| 1125 |
+
}}
|
| 1126 |
+
#color-state-row {{
|
| 1127 |
+
height: 0 !important; overflow: hidden !important;
|
| 1128 |
+
margin: 0 !important; padding: 0 !important; gap: 0 !important;
|
| 1129 |
+
}}
|
| 1130 |
+
#json-paste textarea {{
|
| 1131 |
+
min-height: 120px !important; height: 120px !important; overflow-y: auto !important;
|
| 1132 |
+
}}
|
| 1133 |
+
.upload-container span {{ font-size: 10px !important; }}
|
| 1134 |
+
.upload-container {{ min-height: 130px !important; }}
|
| 1135 |
+
h1, h3 {{ color: {TITLE_COLOR} !important; }}
|
| 1136 |
+
.left-col {{ flex-shrink: 0 !important; }}
|
| 1137 |
+
.step3-row {{ flex-wrap: nowrap !important; }}
|
| 1138 |
+
.step3-row > div {{ min-width: 0 !important; }}
|
| 1139 |
+
.landing-title {{ text-align: center; margin-top: 60px; margin-bottom: 0; }}
|
| 1140 |
+
.landing-title h1 {{ font-size: 2.2em !important; margin-bottom: 0 !important; padding-bottom: 0 !important; }}
|
| 1141 |
+
.landing-sub {{ text-align: center; color: #9ab4d2 !important; font-size: 0.95em;
|
| 1142 |
+
margin-top: 8px; position: relative; z-index: 10; }}
|
| 1143 |
+
.landing-steps {{ margin-top: 40px; }}
|
| 1144 |
+
.step-card {{ text-align: center; padding: 10px; }}
|
| 1145 |
+
.step-card button {{ min-height: 80px !important; white-space: normal !important;
|
| 1146 |
+
line-height: 1.4 !important; padding: 16px 20px !important;
|
| 1147 |
+
background: #283264 !important; border-color: #283264 !important; color: #fff !important; }}
|
| 1148 |
+
.step-card button:hover {{ background: #3a4a8a !important; border-color: #3a4a8a !important; }}
|
| 1149 |
+
.btn-voltar {{ position: absolute; right: 16px; top: 8px; z-index: 100; }}
|
| 1150 |
+
"""
|
| 1151 |
+
) as demo:
|
| 1152 |
+
|
| 1153 |
+
# ══════════════════════════════════════
|
| 1154 |
+
# LANDING PAGE
|
| 1155 |
+
# ══════════════════════════════════════
|
| 1156 |
+
with gr.Column(visible=True) as landing_page:
|
| 1157 |
+
gr.Markdown("# \u2622\ufe0e RADIOTERAPIA.AI \u2014 POP de elite", elem_classes="landing-title")
|
| 1158 |
+
gr.Markdown("**por: Braga, HF.**", elem_classes="landing-sub")
|
| 1159 |
+
|
| 1160 |
+
gr.HTML("<div style='height:50px;'></div>")
|
| 1161 |
+
|
| 1162 |
+
with gr.Row(elem_classes="landing-steps"):
|
| 1163 |
+
with gr.Column(scale=1, elem_classes="step-card"):
|
| 1164 |
+
btn_passo1 = gr.Button(
|
| 1165 |
+
"\U0001f4ac Passo 1: Clique aqui primeiro, passe sua demanda e receba o conte\u00fado do POP (em c\u00f3digo) com o nosso Agente Conselheiro da Qualidade.",
|
| 1166 |
+
variant="primary", size="lg")
|
| 1167 |
+
with gr.Column(scale=1, elem_classes="step-card"):
|
| 1168 |
+
btn_passo2 = gr.Button(
|
| 1169 |
+
"\U0001f4c4 Passo 2: Depois de receber o c\u00f3digo, clique aqui para montar o POP e receber o documento final",
|
| 1170 |
+
variant="primary", size="lg")
|
| 1171 |
+
|
| 1172 |
+
# ══════════════════════════════════════
|
| 1173 |
+
# APP PRINCIPAL (começa oculto)
|
| 1174 |
+
# ══════════════════════════════════════
|
| 1175 |
+
with gr.Column(visible=False) as main_app:
|
| 1176 |
+
with gr.Row():
|
| 1177 |
+
gr.Markdown("# \u2622\ufe0e RADIOTERAPIA.AI \u2014 POP de elite")
|
| 1178 |
+
btn_voltar = gr.Button("\u2190 Voltar", size="sm", variant="secondary", elem_classes="btn-voltar")
|
| 1179 |
+
gr.Markdown("*por: Braga, HF.*\n\n---")
|
| 1180 |
+
|
| 1181 |
+
with gr.Row():
|
| 1182 |
+
|
| 1183 |
+
# ══════════════ COLUNA ESQUERDA ══════════════
|
| 1184 |
+
with gr.Column(scale=3, min_width=580, elem_classes="left-col"):
|
| 1185 |
+
|
| 1186 |
+
# ── ETAPA 1: LOGO (compacto) ──
|
| 1187 |
+
gr.Markdown("### \U0001f5bc\ufe0e Etapa 2.1 \u2014 Logotipo institucional *(opcional)*")
|
| 1188 |
+
logo_input = gr.Image(
|
| 1189 |
+
type="filepath", sources=["upload"],
|
| 1190 |
+
height=130, show_label=False, show_download_button=False
|
| 1191 |
+
)
|
| 1192 |
+
|
| 1193 |
+
gr.Markdown("---")
|
| 1194 |
+
|
| 1195 |
+
# ── ETAPA 2: CORES (4 boxes clicáveis) ──
|
| 1196 |
+
gr.Markdown("### \U0001f3a8\ufe0e Etapa 2.2 \u2014 Paleta de cores")
|
| 1197 |
+
with gr.Row(elem_id="color-state-row"):
|
| 1198 |
+
color_pri = gr.Textbox(value=f"#{init_pal['primary']}", elem_id="pop-hex-pri", show_label=False, container=False)
|
| 1199 |
+
color_sec = gr.Textbox(value="", elem_id="pop-hex-sec", show_label=False, container=False)
|
| 1200 |
+
color_ter = gr.Textbox(value="", elem_id="pop-hex-ter", show_label=False, container=False)
|
| 1201 |
+
color_zeb = gr.Textbox(value="", elem_id="pop-hex-zeb", show_label=False, container=False)
|
| 1202 |
+
|
| 1203 |
+
gr.HTML(f"""
|
| 1204 |
+
<div style="display:flex; gap:6px; height:50px; position:relative;" id="color-boxes">
|
| 1205 |
+
<input type="color" id="cpick-pri" value="#{init_pal['primary']}"
|
| 1206 |
+
style="position:absolute;opacity:0;width:0;height:0;"
|
| 1207 |
+
oninput="
|
| 1208 |
+
var v=this.value, hex=v.replace('#','');
|
| 1209 |
+
document.getElementById('cbox-pri').style.background=v;
|
| 1210 |
+
document.getElementById('cbox-pri').querySelector('span').innerHTML='Prim\\u00e1ria<br>'+v.toUpperCase();
|
| 1211 |
+
var t=document.querySelector('#pop-hex-pri textarea')||document.querySelector('#pop-hex-pri input');
|
| 1212 |
+
if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}
|
| 1213 |
+
var r=parseInt(hex.substr(0,2),16),g=parseInt(hex.substr(2,2),16),b=parseInt(hex.substr(4,2),16);
|
| 1214 |
+
function mx(c,p){{return Math.min(255,Math.max(0,Math.round(c+(255-c)*p)));}}
|
| 1215 |
+
function th(r,g,b){{return '#'+[r,g,b].map(x=>x.toString(16).padStart(2,'0')).join('').toUpperCase();}}
|
| 1216 |
+
var cs=th(mx(r,.45),mx(g,.45),mx(b,.45));
|
| 1217 |
+
var ct=th(mx(r,.70),mx(g,.70),mx(b,.70));
|
| 1218 |
+
var cz=th(mx(r,.85),mx(g,.85),mx(b,.85));
|
| 1219 |
+
[['sec',cs,'Secund\\u00e1ria'],['ter',ct,'Terci\\u00e1ria'],['zeb',cz,'Zebra']].forEach(function(x){{
|
| 1220 |
+
document.getElementById('cbox-'+x[0]).style.background=x[1];
|
| 1221 |
+
document.getElementById('cbox-'+x[0]).querySelector('span').innerHTML=x[2]+'<br>'+x[1];
|
| 1222 |
+
document.getElementById('cpick-'+x[0]).value=x[1];
|
| 1223 |
+
var t2=document.querySelector('#pop-hex-'+x[0]+' textarea')||document.querySelector('#pop-hex-'+x[0]+' input');
|
| 1224 |
+
if(t2){{t2.value=x[1];t2.dispatchEvent(new Event('input',{{bubbles:true}}));}}
|
| 1225 |
+
}});
|
| 1226 |
+
">
|
| 1227 |
+
<div id="cbox-pri" onclick="document.getElementById('cpick-pri').click();"
|
| 1228 |
+
style="flex:1;background:#{init_pal['primary']};border-radius:6px;cursor:pointer;
|
| 1229 |
+
display:flex;align-items:center;justify-content:center;border:2px solid #444;">
|
| 1230 |
+
<span style="color:#fff;font-size:9px;font-weight:bold;text-align:center;pointer-events:none;">
|
| 1231 |
+
Prim\u00e1ria<br>#{init_pal['primary']}</span></div>
|
| 1232 |
+
|
| 1233 |
+
<input type="color" id="cpick-sec" value="#{init_pal['secondary']}"
|
| 1234 |
+
style="position:absolute;opacity:0;width:0;height:0;"
|
| 1235 |
+
oninput="var v=this.value;document.getElementById('cbox-sec').style.background=v;
|
| 1236 |
+
document.getElementById('cbox-sec').querySelector('span').innerHTML='Secund\\u00e1ria<br>'+v.toUpperCase();
|
| 1237 |
+
var t=document.querySelector('#pop-hex-sec textarea')||document.querySelector('#pop-hex-sec input');
|
| 1238 |
+
if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}">
|
| 1239 |
+
<div id="cbox-sec" onclick="document.getElementById('cpick-sec').click();"
|
| 1240 |
+
style="flex:1;background:#{init_pal['secondary']};border-radius:6px;cursor:pointer;
|
| 1241 |
+
display:flex;align-items:center;justify-content:center;">
|
| 1242 |
+
<span style="color:#{init_pal['text_on_secondary']};font-size:9px;font-weight:bold;text-align:center;pointer-events:none;">
|
| 1243 |
+
Secund\u00e1ria<br>#{init_pal['secondary']}</span></div>
|
| 1244 |
+
|
| 1245 |
+
<input type="color" id="cpick-ter" value="#{init_pal['tertiary']}"
|
| 1246 |
+
style="position:absolute;opacity:0;width:0;height:0;"
|
| 1247 |
+
oninput="var v=this.value;document.getElementById('cbox-ter').style.background=v;
|
| 1248 |
+
document.getElementById('cbox-ter').querySelector('span').innerHTML='Terci\\u00e1ria<br>'+v.toUpperCase();
|
| 1249 |
+
var t=document.querySelector('#pop-hex-ter textarea')||document.querySelector('#pop-hex-ter input');
|
| 1250 |
+
if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}">
|
| 1251 |
+
<div id="cbox-ter" onclick="document.getElementById('cpick-ter').click();"
|
| 1252 |
+
style="flex:1;background:#{init_pal['tertiary']};border-radius:6px;cursor:pointer;
|
| 1253 |
+
display:flex;align-items:center;justify-content:center;">
|
| 1254 |
+
<span style="color:#{init_pal['primary_dark']};font-size:9px;font-weight:bold;text-align:center;pointer-events:none;">
|
| 1255 |
+
Terci\u00e1ria<br>#{init_pal['tertiary']}</span></div>
|
| 1256 |
+
|
| 1257 |
+
<input type="color" id="cpick-zeb" value="#{init_pal['quaternary']}"
|
| 1258 |
+
style="position:absolute;opacity:0;width:0;height:0;"
|
| 1259 |
+
oninput="var v=this.value;document.getElementById('cbox-zeb').style.background=v;
|
| 1260 |
+
document.getElementById('cbox-zeb').querySelector('span').innerHTML='Zebra<br>'+v.toUpperCase();
|
| 1261 |
+
var t=document.querySelector('#pop-hex-zeb textarea')||document.querySelector('#pop-hex-zeb input');
|
| 1262 |
+
if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}));}}">
|
| 1263 |
+
<div id="cbox-zeb" onclick="document.getElementById('cpick-zeb').click();"
|
| 1264 |
+
style="flex:1;background:#{init_pal['quaternary']};border-radius:6px;cursor:pointer;
|
| 1265 |
+
display:flex;align-items:center;justify-content:center;">
|
| 1266 |
+
<span style="color:#{init_pal['primary_dark']};font-size:9px;text-align:center;pointer-events:none;">
|
| 1267 |
+
Zebra<br>#{init_pal['quaternary']}</span></div>
|
| 1268 |
+
</div>
|
| 1269 |
+
""")
|
| 1270 |
+
|
| 1271 |
+
btn_reset_colors = gr.Button("\u21ba restaurar cores padr\u00e3o", size="sm", variant="secondary")
|
| 1272 |
+
|
| 1273 |
+
gr.Markdown("---")
|
| 1274 |
+
|
| 1275 |
+
# ── ETAPA 3: JSON (lado a lado) ──
|
| 1276 |
+
gr.Markdown("### \U0001f4dd\ufe0e Etapa 2.3 \u2014 Insira o c\u00f3digo recebido *(upload do arquivo OU cole o c\u00f3digo)*")
|
| 1277 |
+
with gr.Row(equal_height=True, elem_classes="step3-row"):
|
| 1278 |
+
with gr.Column(scale=3):
|
| 1279 |
+
json_file_input = gr.File(
|
| 1280 |
+
file_types=[".json", ".txt"], type="filepath",
|
| 1281 |
+
height=130, show_label=False
|
| 1282 |
+
)
|
| 1283 |
+
with gr.Column(scale=0, min_width=30):
|
| 1284 |
+
gr.HTML('<div style="display:flex;align-items:center;justify-content:center;height:100%;font-weight:700;color:#666;font-size:11px;">OU</div>')
|
| 1285 |
+
with gr.Column(scale=3):
|
| 1286 |
+
json_text_input = gr.Textbox(
|
| 1287 |
+
placeholder='Cole o c\u00f3digo aqui',
|
| 1288 |
+
lines=6, max_lines=6, show_label=False, elem_id="json-paste"
|
| 1289 |
+
)
|
| 1290 |
+
|
| 1291 |
+
# Quando arquivo é carregado, desabilita textbox (mas preserva o valor existente)
|
| 1292 |
+
def toggle_textbox(file, current_text):
|
| 1293 |
+
if file is not None:
|
| 1294 |
+
return gr.Textbox(value=current_text or "", interactive=False,
|
| 1295 |
+
placeholder='Arquivo carregado \u2014 usando arquivo anexo')
|
| 1296 |
+
return gr.Textbox(value=current_text or "", interactive=True,
|
| 1297 |
+
placeholder='Cole o c\u00f3digo aqui')
|
| 1298 |
+
|
| 1299 |
+
json_file_input.change(
|
| 1300 |
+
fn=toggle_textbox,
|
| 1301 |
+
inputs=[json_file_input, json_text_input],
|
| 1302 |
+
outputs=[json_text_input]
|
| 1303 |
+
)
|
| 1304 |
+
|
| 1305 |
+
btn_gerar = gr.Button("\U0001f680 GERAR POP (.docx)", variant="primary", size="lg")
|
| 1306 |
+
btn_novo = gr.Button("\U0001f504 Novo POP \u2014 limpar c\u00f3digo", variant="secondary", size="sm")
|
| 1307 |
+
|
| 1308 |
+
# ══════════════ COLUNA DIREITA ══════════════
|
| 1309 |
+
with gr.Column(scale=2, min_width=280):
|
| 1310 |
+
result_info = gr.Markdown("")
|
| 1311 |
+
btn_download = gr.DownloadButton(
|
| 1312 |
+
"\U0001f4e5 Baixar POP (.docx)", visible=False, variant="primary", size="lg"
|
| 1313 |
+
)
|
| 1314 |
+
preview_gallery = gr.Gallery(
|
| 1315 |
+
label="\u25c4 \u25ba Preview do documento",
|
| 1316 |
+
columns=1, rows=1, height=320,
|
| 1317 |
+
object_fit="contain", visible=False,
|
| 1318 |
+
show_download_button=False
|
| 1319 |
+
)
|
| 1320 |
+
|
| 1321 |
+
# ═══════════ EVENTOS ═══════════
|
| 1322 |
+
|
| 1323 |
+
def generate_preview_images(docx_path):
|
| 1324 |
+
"""Converte .docx → PDF → imagens para preview."""
|
| 1325 |
+
import subprocess, glob
|
| 1326 |
+
try:
|
| 1327 |
+
# Converter docx → pdf via LibreOffice
|
| 1328 |
+
tmp_dir = tempfile.mkdtemp()
|
| 1329 |
+
subprocess.run(
|
| 1330 |
+
["libreoffice", "--headless", "--convert-to", "pdf",
|
| 1331 |
+
"--outdir", tmp_dir, docx_path],
|
| 1332 |
+
capture_output=True, timeout=30
|
| 1333 |
+
)
|
| 1334 |
+
pdf_files = glob.glob(os.path.join(tmp_dir, "*.pdf"))
|
| 1335 |
+
if not pdf_files:
|
| 1336 |
+
return []
|
| 1337 |
+
pdf_path = pdf_files[0]
|
| 1338 |
+
# Converter pdf → imagens via pdftoppm
|
| 1339 |
+
subprocess.run(
|
| 1340 |
+
["pdftoppm", "-jpeg", "-r", "150", pdf_path,
|
| 1341 |
+
os.path.join(tmp_dir, "page")],
|
| 1342 |
+
capture_output=True, timeout=30
|
| 1343 |
+
)
|
| 1344 |
+
imgs = sorted(glob.glob(os.path.join(tmp_dir, "page-*.jpg")))
|
| 1345 |
+
return imgs if imgs else []
|
| 1346 |
+
except Exception:
|
| 1347 |
+
return []
|
| 1348 |
+
|
| 1349 |
+
def on_generate(json_text, json_file, cpri, csec, cter, czeb, logo):
|
| 1350 |
+
fp, info, meta = processar(json_text, json_file, cpri, csec, cter, czeb, logo)
|
| 1351 |
+
if fp:
|
| 1352 |
+
fname = os.path.basename(fp)
|
| 1353 |
+
msg = f"\u2705 POP gerado com sucesso!\n\n`{fname}`"
|
| 1354 |
+
imgs = generate_preview_images(fp)
|
| 1355 |
+
if imgs:
|
| 1356 |
+
return (msg, gr.DownloadButton(value=fp, visible=True),
|
| 1357 |
+
gr.Gallery(value=imgs, visible=True),
|
| 1358 |
+
gr.Button("\U0001f680 GERAR POP (.docx)", interactive=True))
|
| 1359 |
+
return (msg, gr.DownloadButton(value=fp, visible=True),
|
| 1360 |
+
gr.Gallery(visible=False),
|
| 1361 |
+
gr.Button("\U0001f680 GERAR POP (.docx)", interactive=True))
|
| 1362 |
+
return (info or "\u26a0\ufe0f Erro", gr.DownloadButton(visible=False),
|
| 1363 |
+
gr.Gallery(visible=False),
|
| 1364 |
+
gr.Button("\U0001f680 GERAR POP (.docx)", interactive=True))
|
| 1365 |
+
|
| 1366 |
+
def on_start_generate():
|
| 1367 |
+
return ("\u23f3 *Redigindo o POP, aguarde...*",
|
| 1368 |
+
gr.DownloadButton(visible=False),
|
| 1369 |
+
gr.Gallery(visible=False),
|
| 1370 |
+
gr.Button("\u23f3 Redigindo...", variant="secondary", interactive=False))
|
| 1371 |
+
|
| 1372 |
+
btn_gerar.click(
|
| 1373 |
+
fn=on_start_generate,
|
| 1374 |
+
outputs=[result_info, btn_download, preview_gallery, btn_gerar]
|
| 1375 |
+
).then(
|
| 1376 |
+
fn=on_generate,
|
| 1377 |
+
inputs=[json_text_input, json_file_input, color_pri, color_sec, color_ter, color_zeb, logo_input],
|
| 1378 |
+
outputs=[result_info, btn_download, preview_gallery, btn_gerar]
|
| 1379 |
+
)
|
| 1380 |
+
|
| 1381 |
+
def on_novo():
|
| 1382 |
+
return "", None, "", gr.DownloadButton(visible=False), gr.Gallery(visible=False)
|
| 1383 |
+
|
| 1384 |
+
btn_novo.click(
|
| 1385 |
+
fn=on_novo,
|
| 1386 |
+
outputs=[json_text_input, json_file_input, result_info, btn_download, preview_gallery]
|
| 1387 |
+
)
|
| 1388 |
+
|
| 1389 |
+
# Reset cores para padrão
|
| 1390 |
+
def on_reset_colors():
|
| 1391 |
+
p = build_palette(DEFAULT_UI_COLOR)
|
| 1392 |
+
return f"#{p['primary']}", "", "", ""
|
| 1393 |
+
|
| 1394 |
+
btn_reset_colors.click(
|
| 1395 |
+
fn=on_reset_colors,
|
| 1396 |
+
outputs=[color_pri, color_sec, color_ter, color_zeb],
|
| 1397 |
+
js=f"""() => {{
|
| 1398 |
+
var p = '{init_pal["primary"]}', s = '{init_pal["secondary"]}',
|
| 1399 |
+
t = '{init_pal["tertiary"]}', z = '{init_pal["quaternary"]}';
|
| 1400 |
+
var d = '{init_pal["primary_dark"]}', ts = '{init_pal["text_on_secondary"]}';
|
| 1401 |
+
document.getElementById('cbox-pri').style.background='#'+p;
|
| 1402 |
+
document.getElementById('cbox-pri').querySelector('span').innerHTML='Prim\\u00e1ria<br>#'+p;
|
| 1403 |
+
document.getElementById('cpick-pri').value='#'+p;
|
| 1404 |
+
document.getElementById('cbox-sec').style.background='#'+s;
|
| 1405 |
+
document.getElementById('cbox-sec').querySelector('span').innerHTML='Secund\\u00e1ria<br>#'+s;
|
| 1406 |
+
document.getElementById('cpick-sec').value='#'+s;
|
| 1407 |
+
document.getElementById('cbox-ter').style.background='#'+t;
|
| 1408 |
+
document.getElementById('cbox-ter').querySelector('span').innerHTML='Terci\\u00e1ria<br>#'+t;
|
| 1409 |
+
document.getElementById('cpick-ter').value='#'+t;
|
| 1410 |
+
document.getElementById('cbox-zeb').style.background='#'+z;
|
| 1411 |
+
document.getElementById('cbox-zeb').querySelector('span').innerHTML='Zebra<br>#'+z;
|
| 1412 |
+
document.getElementById('cpick-zeb').value='#'+z;
|
| 1413 |
+
}}"""
|
| 1414 |
+
)
|
| 1415 |
+
|
| 1416 |
+
# ═══════════ NAVEGA\u00c7\u00c3O LANDING \u2194 APP ═══════════
|
| 1417 |
+
|
| 1418 |
+
# Passo 1: abre URL do Gemini Gem em nova aba
|
| 1419 |
+
btn_passo1.click(
|
| 1420 |
+
fn=None, inputs=None, outputs=None,
|
| 1421 |
+
js="() => { window.open('https://gemini.google.com/gem/c86826a9110d', '_blank'); }"
|
| 1422 |
+
)
|
| 1423 |
+
|
| 1424 |
+
# Passo 2: oculta landing, mostra app
|
| 1425 |
+
def show_app():
|
| 1426 |
+
return gr.Column(visible=False), gr.Column(visible=True)
|
| 1427 |
+
|
| 1428 |
+
btn_passo2.click(fn=show_app, outputs=[landing_page, main_app])
|
| 1429 |
+
|
| 1430 |
+
# Voltar: oculta app, mostra landing
|
| 1431 |
+
def show_landing():
|
| 1432 |
+
return gr.Column(visible=True), gr.Column(visible=False)
|
| 1433 |
+
|
| 1434 |
+
btn_voltar.click(fn=show_landing, outputs=[landing_page, main_app])
|
| 1435 |
+
|
| 1436 |
+
return demo
|
| 1437 |
+
|
| 1438 |
+
# ══════════════════════════════════════════════════════════════
|
| 1439 |
+
# INICIAR APLICAÇÃO
|
| 1440 |
+
# ══════════════════════════════════════════════════════════════
|
| 1441 |
+
|
| 1442 |
+
demo = criar_interface()
|
| 1443 |
+
demo.launch(share=True, debug=False, show_error=True)
|